From db5c44bf4fdaabea3d1a8b1e5fcd2f61d9b10336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Vassb=C3=B8?= Date: Fri, 11 Oct 2024 14:52:05 +0200 Subject: [PATCH] 1.2.9 (#886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✔ Fixed last lines of Hymnary lyrics sometimes removed - Audio starting/ending actions - Fixed mac online video input paste not setting value - Fixed default folder created when a different path was set - Updated languages * ✨ Duplicated shows remover - Fixed output resolution sometimes incorrect - Tweaks locations of some settings - Existing show alert when creating a new with the same name * ✨ Projects auto scroll to active - RemoteShow tablet mode - Updated RemoteShow slides - JSON bibles without id can be imported - Fixed audio not always playing on first load - Duplicated projects are removed - Projects loading indicator - Delete empty shows * 📄 Fixed OpenSong importing * ✔ Fixed auto size sometimes not working * 📄 Usage log - Auto backups - Template items only shifting around if holding shift key - Auto backup when changing user data location - Unique backup folders - Fixed auto size resize when using custom scripture template style - Fixed output capture not always starting on startup - Improved OpenSong import - Auto size updates - Version update --- package.json | 2 +- public/lang/de.json | 869 +++++++++++------- public/lang/en.json | 23 +- public/lang/hu.json | 29 +- .../capture/helpers/CaptureTransmitter.ts | 1 + src/electron/cloud/drive.ts | 2 +- src/electron/data/backup.ts | 17 +- src/electron/data/export.ts | 19 +- src/electron/data/store.ts | 3 + src/electron/index.ts | 11 +- src/electron/utils/LyricSearch.ts | 113 ++- src/electron/utils/files.ts | 21 +- src/electron/utils/responses.ts | 95 +- src/electron/utils/shows.ts | 152 +++ src/frontend/classes/Show.ts | 11 + .../components/actions/customActivation.ts | 2 + src/frontend/components/drawer/Drawer.svelte | 4 +- .../components/drawer/Navigation.svelte | 2 +- .../components/drawer/pages/Templates.svelte | 2 +- src/frontend/components/helpers/array.ts | 18 + src/frontend/components/helpers/audio.ts | 36 +- .../components/helpers/historyActions.ts | 3 +- src/frontend/components/helpers/output.ts | 33 +- src/frontend/components/helpers/time.ts | 7 + src/frontend/components/inputs/Button.svelte | 1 + .../components/inputs/MediaPicker.svelte | 3 +- .../components/inputs/TextArea.svelte | 5 +- .../components/inputs/TextInput.svelte | 2 +- src/frontend/components/main/Toast.svelte | 3 +- .../main/popups/CreatePlayer.svelte | 5 +- .../main/popups/DeleteDuplicatedShows.svelte | 262 ++++++ .../components/main/popups/ResetAll.svelte | 1 + .../main/popups/createShow/CreateShow.svelte | 9 +- src/frontend/components/output/Output.svelte | 20 +- .../output/layers/SlideContent.svelte | 3 +- .../components/output/preview/Preview.svelte | 9 + .../output/preview/PreviewCanvas.svelte | 1 + .../output/preview/PreviewOutput.svelte | 3 +- .../transitions/SlideItemTransition.svelte | 19 +- .../components/settings/tabs/Cloud.svelte | 17 +- .../settings/tabs/Connection.svelte | 15 +- .../components/settings/tabs/General.svelte | 80 +- .../components/settings/tabs/Groups.svelte | 50 +- .../components/settings/tabs/Other.svelte | 246 +++-- .../components/settings/tabs/Outputs.svelte | 5 +- .../components/settings/tabs/Styles.svelte | 112 +-- .../components/show/ProjectList.svelte | 39 +- src/frontend/components/show/Projects.svelte | 27 +- .../components/show/tools/Media.svelte | 4 +- .../components/show/tools/Metadata.svelte | 26 +- src/frontend/components/slide/Textbox.svelte | 35 +- src/frontend/components/slide/Zoomed.svelte | 4 +- .../components/system/Autoscroll.svelte | 3 +- src/frontend/converters/bible.ts | 5 +- src/frontend/converters/opensong.ts | 50 +- src/frontend/stores.ts | 1 + src/frontend/utils/popup.ts | 2 + src/frontend/utils/receivers.ts | 11 +- src/frontend/utils/save.ts | 24 +- src/frontend/utils/startup.ts | 25 +- src/frontend/utils/updateSettings.ts | 2 +- src/server/remote/App.svelte | 27 +- src/server/remote/components/Button.svelte | 2 +- src/server/remote/components/Tabs.svelte | 67 +- .../remote/components/slide/ShowSlide.svelte | 15 +- .../remote/components/slide/Slide.svelte | 5 +- .../remote/components/slide/Slides.svelte | 4 +- .../components/tablet/TabletMode.svelte | 410 +++++++++ src/types/Main.ts | 1 + src/types/Show.ts | 2 +- 70 files changed, 2271 insertions(+), 866 deletions(-) create mode 100644 src/electron/utils/shows.ts create mode 100644 src/frontend/components/main/popups/DeleteDuplicatedShows.svelte create mode 100644 src/server/remote/components/tablet/TabletMode.svelte diff --git a/package.json b/package.json index e873dc2c..f1cfc7aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freeshow", - "version": "1.2.9-beta.1", + "version": "1.2.9", "private": true, "main": "build/electron/index.js", "description": "Show song lyrics and more for free!", diff --git a/public/lang/de.json b/public/lang/de.json index 42048de3..74874786 100644 --- a/public/lang/de.json +++ b/public/lang/de.json @@ -2,30 +2,54 @@ "main": { "welcome": "Willkommen", "quit": "Beenden", - "docs": "Doku", + "docs": "Dokumentation", "about": "Über", "unnamed": "Unbenannt", "drop": "Hier fallenlassen", "search": "Suche", + "quick_search": "Schnellsuche", "none": "Nichts", - "finished": "Finished", - "system_open": "Open in system" + "finished": "Fertig", + "open": "Öffnen", + "system_open": "Mit Standardprogramm öffnen" + }, + "guide": { + "start": "Kurzanleitung", + "skip": "Überspringen" + }, + "titlebar": { + "file": "Datei", + "edit": "Bearbeiten", + "view": "Ansicht", + "help": "Hilfe" + }, + "screen": { + "width": "Breite", + "height": "Höhe", + "pixels": "Pixel", + "top": "Oben", + "right": "Rechts", + "bottom": "Unten", + "left": "Links" }, "about": { "check_updates": "Auf Updates prüfen", "made": "Erstellt in Norwegen von", - "report": "Ein Problem melden? Erstelle einen Eintrag bei GitHub", - "help": "Möchtest du beim Übersetzen helfen oder eine Erweiterung anfragen? Schreibe eine E-Mail an", + "more": "Schau dir auch unsere anderen Programme an ", + "report": "Um ein Problem zu melden oder eine neue Funktion vorzuschlagen, gehe zu", + "translate": "Möchtest du beim Übersetzen helfen? Gehe zu", + "mail": "Kontakt per E-Mail", + "support": "Wenn Sie für dieses Projekt dankbar sind, erwäge es zu unterstützen", "assets": "Verwendete Erweiterungen", "libraries": "Verwendete Bibliotheken", "thanks": "Dank an", "new_update": "Neues Update verfügbar", - "download": "Zum Download gehe zu freeshow.app", + "download": "Starte das Programm für das Update neu oder gehe zu freeshow.app für einen manuellen Download", "changes": "Was ist neu" }, "tooltip": { - "project": "Erstelle ein neues Projekt, wo du Shows hinzufügen und anordnen kannst.", - "show": "Erstelle eine neue Show, zu der du Liedtexte, Präsentationen und Medien hinzufügen kannst. Halte Strg/Cmd gedrückt, um eine leere Show zu erstellen.", + "project": "Erstelle ein neues Projekt, in das du Shows hinzufügen und anordnen kannst.", + "show": "Erstelle eine neue Show, zu der du Liedtexte, Präsentationen und Medien hinzufügen kannst.", "groups": "Alle Gruppen in der aktuellen Show und alle globalen Gruppen. Klicke oder ziehe sie, um sie dem aktuellen Layout hinzuzufügen.", "layout": "Füge Übergänge und Übergangszeiten zu Folien im aktuellen Layout hinzu.", "media": "Alle Medien in der aktuellen Show. Zeige sie an, oder ziehe sie in das aktuelle Layout.", @@ -39,8 +63,11 @@ "options": "Mehr Optionen.", "scripture": "Halte Strg/Cmd oder Shift gedrückt, um mehrere Verse auszuwählen." }, + "tips": { + "trigger": "Trigger werden oft genutzt, um HTTP-Anfragen zu senden, die die Kamera Presets auslöst." + }, "setup": { - "good_luck": "Ich hoffe du findest diese Software hilfreich. Viel Erfolg bei der Anzeige! :)", + "good_luck": "Ich hoffe du findest diese Software hilfreich. Viel Erfolg beim Präsentieren! :)", "tips": "Finde nützliche Tipps und Tutorials auf der Webseite.", "change_later": "Du kannst diese Einstellungen später ändern", "get_started": "Fang an!" @@ -49,7 +76,7 @@ "welcome": "Willkommen", "meetings": "Treffen", "example": "Beispiel", - "example_note": "Schreibe Notiz hier", + "example_note": "Schreibe hier deine Notizen", "watermark": "Wasserzeichen", "recording": "Aufnahme", "clock": "Uhr", @@ -62,27 +89,20 @@ "small": "Klein", "bold": "Fett" }, - "titlebar": { - "file": "Datei", - "edit": "Bearbeiten", - "view": "Ansicht", - "help": "Hilfe" - }, - "screen": { - "width": "Breite", - "height": "Höhe", - "pixels": "Pixel", - "top": "Top", - "right": "Right", - "bottom": "Bottom", - "left": "Left" - }, "create_show": { - "search_web": "Search for song on the web", + "web": "Websuche", + "search_web": "Suche den Song im Web", + "search_results": "Suchergebnisse", "more_options": "Mehr Optionen", "format_new_show": "Text formatieren", + "format_new_show_tip": "Textformatierung durch automatische Großschreibung, Textaufteilung, Gruppenzuordnung & mehr verbessern.", "split_lines": "Anzahl der Zeilen", - "quick_lyrics_example_text": "Zeile" + "split_lines_tip": "Anzahl der pro Folie erlaubten Zeilen vor der automatischen Textaufteilung", + "quick_lyrics": "Schnellbearbeitung Liedtexte", + "quick_lyrics_tip": "Text einfügen oder manuell eingeben", + "quick_lyrics_example_tip": "Liedtexte oder anderes eingeben", + "quick_lyrics_example_text": "Zeile", + "empty": "Leere Show" }, "preview": { "_previous_show": "Vorherige Show", @@ -93,19 +113,20 @@ "_update": "Ausgabe aktualisieren", "_next_slide": "Nächste Folie", "_next_show": "Nächste Show", - "_hide_preview": "Hide preview", - "show_preview": "Show preview", - "restore_output": "Restore output", - "enable_controls": "Show media preview controls", + "_hide_preview": "Vorschau ausblenden", + "show_preview": "Vorschau einblenden", + "restore_output": "Ausgabe wiederherstellen", + "enable_controls": "Steuerelemente anzeigen", "background": "Hintergrund", - "foreground": "Foreground", + "foreground": "Vordergrund", "slide": "Folie", "overlays": "Überlagerungen", "audio": "Audio", "to_start": "Gehe zum Start", "nextTimer": "Timer zur nächsten Folie", "lock": "Sperren", - "unlock": "Entsperren" + "unlock": "Entsperren", + "test_pattern": "Testbild" }, "clear": { "all": "Alle freigeben", @@ -113,7 +134,8 @@ "slide": "Folie freigeben", "overlays": "Überlagerungen freigeben", "audio": "Audio freigeben", - "nextTimer": "Timer zur nächsten Folie freigeben" + "nextTimer": "Timer zur nächsten Folie freigeben", + "drawing": "Zeichnung löschen" }, "remove": { "background": "Entferne Hintergrund", @@ -127,26 +149,47 @@ "media": { "_loop": "Schleife", "play": "Abspielen", + "play_multiple": "Mehrere abspielen", + "toggle_shuffle": "Zufallswiedergabe an/aus", + "next": "Weiter", + "previous": "Zurück", "play_no_filters": "Abspielen ohne Filter", "favourite": "Favorit", "pause": "Pause", "stop": "Stop", - "back10": "Go back 10 seconds", - "forward10": "Go forward 10 seconds", + "back10": "10 Sekunden zurück", + "forward10": "10 Sekunden vor", "volume": "Lautstärke", "gain": "Gain", "speed": "Speed", "show": "Show", - "flip": "Umdrehen", + "flip": "Spiegeln", + "flip_horizontally": "Horizontal spiegeln", + "flip_vertically": "Vertikal spiegeln", "all": "Ordner, Bilder & Videos", "folder": "Nur Ordner", "image": "Nur Bilder", "video": "Nur Videos", - "fit": "Anpassen", + "fit": "Skalierung", "contain": "Enthalten", - "fill": "Ausfüllen", - "cover": "Überdecken", - "online": "Online" + "fill": "verzerrt ausfüllen", + "cover": "abgeschnitten ausfüllen", + "online": "Online", + "recommended": "Empfohlen", + "bundle_media_files": "Paket aller Mediendateien", + "bundle_media_files_tip": "Mediendateien aller Shows in einen gemeinsamen Ordner kopieren" + }, + "audio": { + "settings": "Audio Einstellungen", + "playlist_settings": "Einstellungen der Wiedergabeliste", + "custom_output": "Benutzerdefinierte Audioausgabe", + "mute_when_video_plays": "Stummschalten wenn ein Video läuft", + "pre_fader_volume_meter": "Pre-Fader Lautstärkeanzeige", + "metronome": "Metronom", + "toggle_metronome": "Metronom umschalten", + "tempo": "Tempo", + "bpm": "BPM", + "beats": "Beats" }, "menu": { "show": "Show", @@ -157,12 +200,13 @@ "_title_stage": "Bühnenansicht", "draw": "Zeichnen", "_title_draw": "Zeichnen", - "calendar": "Kalendar", + "calendar": "Kalender", "_title_calendar": "Veranstaltungen", "settings": "Einstellungen", "_title_settings": "Einstellungen", "_title_display": "Anzeigen", - "_title_display_stop": "Präsentation stoppen" + "_title_display_stop": "Präsentation stoppen", + "again_confirm": "Zum Bestätigen erneut klicken" }, "empty": { "general": "Nichts hier", @@ -180,9 +224,9 @@ "groups": "Keine Gruppen", "events": "Keine Veranstaltungen", "text": "Etwas eingeben", - "timers": "No timers", - "input": "Missing value in input", - "recording": "Right click a screen or window to start a recording." + "timers": "Keine Timer", + "input": "Fehlender Wert in der Eingabe", + "recording": "Rechtsklick auf einen Bildschirm oder ein Fenster, um eine Aufnahme zu starten." }, "remote": { "projects": "Projekte", @@ -193,11 +237,12 @@ "lyrics": "Liedtexte", "end": "Ende", "no_output": "Keine Ausgabe", - "remember": "Erinnere dich an mich", + "remember": "Angemeldet bleiben", "loading": "Laden...", "submit": "Bestätigen", "password": "Passwort", - "wrong_password": "Falsches Passwort" + "wrong_password": "Falsches Passwort", + "quick_play": "Schnelle Wiedergabe" }, "error": { "no_show": "Kann Show nicht finden", @@ -208,8 +253,9 @@ "not_found": "Nicht gefunden", "display": "Kann das Ausgabefenster der Show nicht auf aktuellem Bildschirm wiedergeben.", "keep_one_layout": "Du brauchst mindestens ein Layout", - "video_unavailable": "Video herunterladen nicht verfügbar? Der Ersteller hat das Einbetten von Videos deaktiviert.", - "folder_exists": "Der Ordner besteht bereits" + "video_unavailable": "Video nicht verfügbar? Der Ersteller hat das Einbetten von Videos deaktiviert.", + "folder_exists": "Der Ordner besteht bereits", + "uri": "Der Audioname konnte nicht geparst werden, bitte benenne die Datei um" }, "meta": { "title": "Titel", @@ -220,17 +266,21 @@ "copyright": "Copyright", "CCLI": "Lizenz (CCLI)", "year": "Jahr", + "key": "Tonart", "message": "Mitteilung", - "message_tip": "Display something on all slides", - "auto_media": "Get meta from media content", - "override_output": "Override styling in output", + "message_tip": "Zeige etwas auf allen Folien", + "auto_media": "Metadaten aus den Medieninhalt auslesen", + "override_output": "Ausgabestil überschreiben", "display_metadata": "Zeige Metadaten", - "meta_template": "Metadata template", - "text_divider": "Text separator", - "message_template": "Message template" + "meta_template": "Metadaten Vorlage", + "text_divider": "Text Trennzeichen", + "message_template": "Mitteilung Vorlage", + "tags": "Tags", + "new_tag": "Neuer Tag", + "clear_tag_filter": "Tagfilterung entfernen" }, "show_at": { - "never": "Kein Folien", + "never": "Keine Folien", "always": "Alle Folien", "first": "Erste Folie", "last": "Letzte Folie", @@ -256,16 +306,16 @@ "hover": "Maus über Objekt", "focus": "Fokus" }, - "buttons": { - "changeTheme": "Design wechseln" - }, "inputs": { "name": "Name", "url": "URL", - "method": "Übertragungs Methode", + "method": "HTTP Methode", + "contentType": "Content-Type", "payload": "Daten", "video_id": "Video ID/URL", "close_ad": "Anzeige auf dem Ausgabebildschirm schließen", + "start": "Start", + "end": "Ende", "change_folder": "Wähle einen anderen Speicherort" }, "tabs": { @@ -273,13 +323,16 @@ "media": "Medien", "overlays": "Überlagerungen", "audio": "Audio", - "effects": "Effects", + "effects": "Effekte", "scripture": "Bibel", - "calendar": "Kalendar", + "calendar": "Kalender", + "functions": "Funktionen", + "actions": "Aktionen", "player": "Player", "live": "Live", "timers": "Timer", - "variables": "Variables", + "variables": "Variablen", + "triggers": "Trigger", "templates": "Vorlagen", "web": "Web" }, @@ -302,34 +355,40 @@ "groups": { "current": "Aktuell", "global": "Global", - "toggle_global_group": "Toggle global groups", + "toggle_global_group": "Globale Gruppen ein-/ausblenden", + "group_shortcut": "Gruppen Tastaturkürzel", + "group_template": "Gruppen Vorlage", "intro": "Intro", "verse": "Vers", - "pre_chorus": "Pre Chorus", + "pre_chorus": "Prechorus", "chorus": "Refrain", - "break": "Break", + "break": "Pause", "tag": "Tag", "bridge": "Bridge", "outro": "Outro" }, "popup": { "show": "Neue Show", - "select_show": "Select show", + "select_show": "Show auswählen", "rename": "Umbenennen", "color": "Farbe", - "find_replace": "Find and replace", - "edit_list": "Edit list", + "find_replace": "Suchen und ersetzen", + "edit_list": "Änderungsliste", "timer": "Timer", "variable": "Variable", + "trigger": "Trigger", + "audio_stream": "Audiostream", "transition": "Übergang", "delete_show": "Show löschen", "delete_show_confirmation": "Bist du sicher, dass du löschen möchtest?", - "change_name": "Change name on", + "change_name": "Folgendes umbenennen", "choose_screen": "Wähle Bildschirm", - "change_output_values": "Change output values", - "choose_style": "Choose style", + "choose_output": "Ausgabetyp auswählen", + "change_output_values": "Ausgabe Position", + "choose_chord": "Akkord auswählen", "set_time": "Set time", "animate": "Animate", + "translate": "Lokalisierung", "next_timer": "Timer zur nächsten Folie", "import": "Importieren", "songbeamer_import": "Songbeamer Importieren", @@ -339,17 +398,18 @@ "player": "Player", "edit_event": "Veranstaltung bearbeiten", "about": "Über", - "history": "History", - "midi": "MIDI", - "connect": "Connect", - "cloud_update": "Syncing with cloud", - "cloud_method": "Data location", + "history": "Verlauf", + "action": "Aktion", + "connect": "Verbinden", + "cloud_update": "Mit der Cloud synchronisieren", + "cloud_method": "Dateispeicherort", "shortcuts": "Tastaturkürzel", "icon": "Symbole", - "manage_icons": "Manage icons", - "choose_camera": "Chooose camera", + "manage_icons": "Icons verwalten", + "manage_colors": "Farben verwalten", + "choose_camera": "Kamera auswählen", "initialize": "Willkommen bei FreeShow", - "unsaved": "Du hast noch nicht gespeichert! Bist du dir sicher, dass du beenden möchtest?", + "unsaved": "Programm beenden?", "cancel": "Abbrechen", "continue": "Weiter", "reset_all": "Alles zurücksetzen", @@ -359,41 +419,46 @@ "save_quit": "Speichern und Beenden" }, "toast": { - "saving": "Saving...", - "saved": "Saved", - "error_media": "Could not get media.", - "empty_styles": "No styles", - "chapter_undefined": "Chapter {} does not exist in this book.", - "verse_undefined": "Verse {} does not exist in this chapter.", - "recording_started": "Recording started!", - "recording_stopped": "Recording stopped!", - "starting_show": "Starting show", - "less_than_minute": "in less than a minute.", - "less_than_seconds": "in less than {} seconds.", - "now": "now!", - "no_video_id": "No video ID", - "no_name": "No name", - "media_replaced": "Missing media file replaced with match.", - "lyrics_undefined": "Could not find any lyrics!", - "lyrics_copied": "Lyrics copied from ", - "no_pdf_linux": "Can't export as PDF on Linux.", - "one_output": "You have to have at least one active output!", - "empty_cache": "Cache is empty.", - "deleted_cache": "Deleted media thumbnail cache.", - "no_songswords_easyworship": "Missing SongsWords.db file.", - "delete_shows_empty": "No shows to delete.", - "midi_no_project": "Received MIDI to change project, but no project found at index:", - "midi_no_show": "Received MIDI to start slide, but no show active.", - "midi_no_slide": "Received MIDI to start slide, but no slide found at index:", - "midi_no_velocity": "Received MIDI signal, but no velocity, defaults to first index." + "saving": "Speichern…", + "saved": "Gespeichert", + "error_media": "Medien können nicht geladen werden", + "empty_styles": "Keine Styles", + "chapter_undefined": "Kapitel {} existiert in diesem Buch nicht.", + "verse_undefined": "Vers {} existiert in diesem Kapitel nicht.", + "recording_started": "Aufnahme gestartet!", + "recording_stopped": "Aufnahme gestoppt!", + "starting_action": "Aktion starten", + "less_than_minute": "in weniger als einer Minute.", + "less_than_seconds": "in weniger als {} Sekunden.", + "now": "jetzt!", + "no_video_id": "Keine Video-ID", + "no_name": "Kein Name", + "reverting_setting": "Änderung wird in {} Sekunden rückgängig gemacht, erneut aktivieren, um die Änderung zu behalten!", + "reverted": "Änderung rückgängig gemacht! Nur erneut aktivieren, wenn keine Probleme aufgetreten sind.", + "media_replaced": "Fehlende Mediendatei wurde ersetzt.", + "lyrics_undefined": "Keinen Liedtext gefunden!", + "lyrics_copied": "Liedtext kopiert von", + "one_output": "Es muss mindestens eine Ausgabe aktiv sein!", + "empty_cache": "Cache ist leer.", + "deleted_cache": "Medienvorschaubild-Cache gelöscht.", + "no_songswords_easyworship": "Fehlende SongsWords.db Datei.", + "delete_shows_empty": "Keine Shows zu löschen.", + "midi_no_project": "Trigger für Projektwechsel empfangen, aber kein Projekt an Index gefunden:", + "midi_no_show": "Trigger für Folie empfangen, aber keine Show ist aktiv:", + "midi_no_slide": "Trigger für Folie empfangen, aber keine Folie an Index gefunden:", + "midi_no_velocity": "MIDI Signal ohne Velocity erhalten, verwende standardmäßig den ersten Index." }, "new": { + "create": "Anlegen", "show": "Neue Show", "empty_show": "Neue, leere Show", "project": "Neues Projekt", - "section": "New section", + "section": "Neue Sektion", "timer": "Neuer Timer", - "variable": "New variable", + "variable": "Neue Variable", + "trigger": "Neuer Trigger", + "audio_stream": "Neuer Audio Stream", + "playlist": "Neue Wiedergabeliste", "category": "Neue Kategorie", "private": "Neue private Show", "folder": "Neuer Ordner", @@ -401,44 +466,51 @@ "overlay": "Neue Überlagerung", "template": "Neue Vorlage", "scripture": "Neue Bibel", - "collection": "New collection", + "collection": "Neue Sammlung", + "action": "Neue Aktion", "event": "Neue Veranstaltung" }, "show": { "name": "Name", "category": "Kategorie", - "quick_lyrics": "Schnelle Liedtexte", "new_layout": "Neues Layout", "grid": "Kachelanzeige", - "simple": "Simple view", + "simple": "Einfache Anzeige", "list": "Listenanzeige", "lyrics": "Liedtext-Anzeige", - "text": "Text edit", - "update": "Update show" + "text": "Text Bearbeiten", + "update": "Show aktualisieren", + "locked": "Diese Show wurde gesperrt!", + "locked_info": "Diese Show wurde für die Bearbeitung gesperrt! Sperre zunächst in der Shows-Übersicht aufheben.", + "slide_template": "Folien Vorlage", + "source": "Quelle", + "artist": "Künstler", + "song": "Lied" }, "actions": { "rename": "Umbenennen", "recolor": "Farbe wechseln", "remove": "Entfernen", - "remove_group": "Remove group", - "choose_group": "Choose group", - "goto_group": "Go to group", - "change_output_style": "Change output style", - "active_outputs": "Active outputs", - "all_outputs": "All outputs", - "specific_outputs": "Specific outputs", + "remove_group": "Gruppe entfernen", + "choose_group": "Gruppe wählen", + "goto_group": "Zu Gruppe wechseln", + "active_outputs": "Aktive Ausgaben", + "all_outputs": "Alle Ausgaben", + "specific_outputs": "Spezifische Ausgaben", "toggle_private": "Privat", - "view_private": "Show private", + "view_private": "Private Shows anzeigen", "import": "Importieren", "export": "Exportieren", "duplicate": "Duplizieren", "delete": "Löschen", + "delete_slide": "Folie löschen", + "delete_group": "Gruppe löschen", "delete_all": "Alle Löschen", "close": "Schließen", "save": "Speichern", - "done": "Done", + "done": "Fertig", "disable": "Deaktivieren", - "enable": "Enable", + "enable": "Aktivieren", "undo": "Rückgängig", "redo": "Wiederholen", "cut": "Ausschneiden", @@ -446,96 +518,177 @@ "paste": "Einfügen", "pasteAndMatchStyle": "Einfügen und Stil anpassen", "selectAll": "Alle Auswählen", - "remove_selection": "Remove selection", + "remove_selection": "Auswahl entfernen", "speech": "Sprechen", "startSpeaking": "Sprechen beginnen", "stopSpeaking": "Sprechen beenden", + "focus_mode": "Fokusmodus umschalten", "fullscreen": "Vollbild umschalten", "resetZoom": "Zoom zurücksetzen", "zoom": "Zoom", "zoomIn": "Vergrößern", "zoomOut": "Verkleinern", "reset": "Zurücksetzen", - "reset_defaults": "Reset defaults", + "convert_to_images": "In Bilder konvertieren", + "converting": "Konvertieren...", + "remove_template_from_show": "Vorlage aus der Show entfernen", + "reset_defaults": "Beispiele zurücksetzen", "to_all": "Auf alle anwenden", - "to_following": "Apply to following", + "to_following": "Auf Folgende anwenden", "back": "Zurück", "home": "Home", "mute": "Stummschalten", "unmute": "Aufheben der Stummschaltung", - "toggle_time_marker": "Toggle time markers", - "add_time_marker": "Add time marker", - "bind_to": "Bind to", - "remove_binding": "Remove binding", - "show_timer": "Time until show", - "hide_timer": "Time until hide", - "choose_custom": "Choose custom", + "increase_volume": "Lautstärke erhöhen", + "decrease_volume": "Lautstärke verringern", + "toggle_time_marker": "Zeitmarkierung umschalten", + "add_time_marker": "Zeitmarkierung hinzufügen", + "bind_to": "Spezifische Ausgaben", + "remove_binding": "Spezifische Ausgaben enfernen", + "dynamic_values": "Dynamische Werte", + "rearrange": "Neu anordnen", + "to_front": "In den Vordergrund", + "forward": "Ebene nach vorne", + "backward": "Ebene nach hinten", + "to_back": "In den Hintergrund", + "show_timer": "Zeit bis zum Anzeigen", + "hide_timer": "Zeit bis zum Ausblenden", + "choose_custom": "Benutzerdefiniert auswählen", + "add_color": "Farbe hinzufügen", "format": "Format", - "find_replace": "Find and replace text", - "cut_in_half": "Cut in half", - "find": "Find", - "replace": "Replace", - "case_sensitive": "Case sensitive", + "find_replace": "Text finden und ersetzen", + "cut_in_half": "Halbieren", + "merge": "Zusammenführen", + "find": "Finden", + "replace": "Ersetzen", + "case_sensitive": "Groß-/Kleinschreibung beachten", "uppercase": "Alles großschreiben", "lowercase": "Alles kleinschreiben", "capitalize": "Großschreiben", - "trim": "Trimme", - "click_disable": "Click any to disable", - "svg_clipboard": "Import SVG from clipboard", + "trim": "Trimmen", + "click_disable": "Zum Deaktivieren anklicken", + "svg_clipboard": "SVG aus der Zwischenablage importieren", "fullscreen_preview": "Vollbildvorschau umschalten", "toggle_output": "Ausgabebildschirm umschalten", + "toggle_panels": "Panels umschalten", "change_tab": "Tab wechseln", - "change_drawer_tab": "Zeichen-Tab wechseln", - "toggle_drawer": "Toggle drawer", - "actions": "Aktionen", - "clear_history": "Clear history", - "set_key": "Set key", - "custom_key": "Set custom value", - "play_on_midi": "Play on MIDI in", - "send_midi": "Send MIDI out", - "delete_shows_not_indexed": "Delete shows in 'Shows' folder that are not indexed", - "delete_thumbnail_cache": "Delete thumbnail cache", - "open_log_file": "Open log file", - "refresh_all_shows": "Get all shows in 'Shows' folder", - "start_timer": "Start timer", - "stop_timers": "Stop active timers", - "next_after_media": "Next after media", + "change_drawer_tab": "Übersichtstab wechseln", + "change_slide": "Folie wechseln", + "change_project_item": "Projektelement ändern", + "change_drawer_item": "Übersichtselement ändern", + "change_drawer_category": "Übersichtskategorie wechseln", + "toggle_drawer": "Übersicht ein-/ausblenden", + "slide_actions": "Folien Aktionen", + "item_actions": "Element Aktionen", + "clear_history": "Verlauf löschen", + "chord_info": "Beliebigen Buchstaben anklicken, um einen Akkord hinzuzufügen.", + "chord_key": "Tonart", + "chord_type": "Typ", + "chord_tension": "Optionston", + "chord_bass": "Bass", + "roman_keys": "Stufenakkorde", + "set_key": "Noten", + "custom_key": "Benutzerdefinierter Text", + "select_chord": "Diesen Akkord auswählen", + "play_on_midi": "Durch MIDI Signal aktivieren", + "play_on_midi_tip": "Aktiviere diese Folie, wenn das gewählte MIDI Signal empfangen wird", + "send_midi": "Sende MIDI Signal", + "delete_shows_not_indexed": "Nicht indizierte Shows im \"Shows\"-Ordner löschen", + "delete_thumbnail_cache": "Vorschaubild-Cache löschen", + "open_log_file": "Logdatei öffnen", + "open_cache_folder": "Cache Ordner öffnen", + "refresh_all_shows": "Alle Shows aus dem \"Shows\"-Ordner laden", + "start_timer": "Timer starten", + "stop_timers": "Aktive Timer stoppen", + "next_after_media": "Weiter nach der Wiedergabe", "remove_media": "Medien entfernen", - "remove_layers": "Remove layers", - "index_select_project": "Select project by index", - "index_select_project_show": "Select project item by index", - "index_select_slide": "Select slide by index", - "start_recording": "Start recording", - "stop_recording": "Stop recording" + "remove_layers": "Layer entfernen", + "start_recording": "Aufnahme starten", + "stop_recording": "Aufnahme beenden", + "export_recording": "Aufnahme beenden und exportieren", + "index_select_project": "Projekt nach Index auswählen", + "next_project_item": "Nächstes Projektelement", + "previous_project_item": "Vorheriges Projektelement", + "index_select_project_item": "Projektelement nach Index auswählen", + "name_select_show": "Show nach Name auswählen", + "random_slide": "Zufällige Folie anzeigen", + "index_select_slide": "Folie nach Index auswählen", + "name_select_slide": "Folie nach Name auswählen", + "toggle_output_lock": "Ausgabesperre umschalten", + "toggle_output_windows": "Ausgabefenster umschalten", + "id_select_group": "Gruppe nach ID auswählen", + "id_change_stage_layout": "Bühnenlayout nach ID auswählen", + "start_camera": "Kamera starten", + "index_select_overlay": "Überlagerung nach Index auswählen", + "name_select_overlay": "Überlagerung nach Name auswählen", + "change_volume": "Lautstärke ändern", + "start_audio_stream": "Audio Stream starten", + "start_playlist": "Wiedergabeliste starten", + "playlist_next": "Nächster Titel in der Wiedergabeliste", + "start_metronome": "Metronom starten", + "name_start_timer": "Timer über den Namen starten", + "id_start_timer": "Timer über die ID starten", + "start_slide_timers": "Timer der aktiven Folie starten", + "id_select_output_style": "Ausgabestil nach ID auswählen", + "change_output_style": "Ausgabestil ändern", + "change_stage_output_layout": "Layout des Ausgabestils ändern", + "change_transition": "Übergang ändern", + "change_variable": "Variable ändern", + "start_trigger": "Starte Trigger", + "run_action": "Aktion ausführen", + "toggle_action": "Aktion umschalten", + "send_rest_command": "HTTP-Anfrage senden", + "custom_activation": "Benutzerdefinierte Aktivierung", + "activate_on_startup": "Beim Programmstart aktivieren", + "activate_save": "Beim Speichern aktivieren", + "activate_slide_clicked": "Bei Klick auf Folie aktivieren", + "activate_video_starting": "Aktivieren wenn Video startet", + "activate_video_ending": "Aktivieren wenn Video endet", + "activate_timer_ending": "Aktivieren wenn Timer endet", + "activate_scripture_start": "Aktivieren, wenn Bibel gestartet wurde", + "activate_slide_cleared": "Aktivieren, wenn Folie entfernt wurde", + "activate_background_cleared": "Aktivieren, wenn der Hintergrund entfernt wurde", + "activate_show_created": "Beim Erstellen einer Show aktivieren", + "activate_audio_playlist_ended": "Nach dem Ende einer Wiedergabeliste aktivieren" + }, + "recording": { + "remove": "Aufnahme löschen", + "tip": "Timing der Folien aufnehmen und wiedergeben. Mit einer Tonspur auf der ersten Folie synchronisieren.", + "layout_changed": "Layout hat sich seit der letzten Aufnahme verändert!", + "audio_synced": "Mit Audio synchronisiert!", + "start": "Folienaufnahme starten" }, "animate": { - "change": "Change", - "set": "Set", - "wait": "Wait", - "background": "background", - "text": "text", - "item": "item", - "to": "to", - "for": "for", - "seconds": "seconds" + "change": "Ändere", + "set": "Setze", + "wait": "Warte", + "background": "Hintergrund", + "text": "Text", + "item": "Element", + "to": "zu", + "for": "für", + "seconds": "Sekunden" }, "cloud": { - "info": "Sync files with Google Drive for backups, or if you work on multiple computers.", - "tip_api": "You have to provide your own free Google API key so the program can automatically upload files to your Google Drive.", - "tip_how": "Don't know how to get one?", - "tip_guide": "Click here for a guide.", - "enable": "Automatic sync on startup and save", - "disable_upload": "Disable uploading data", - "media_id": "Media path ID", - "google_drive_api": "Google API service account key", - "select_key": "Import keys file", - "update_key": "Update keys file", - "main_folder": "Set main folder manually", - "media_folder": "Cloud media folder", - "reconnect": "Reconnect", - "sync": "Sync", - "choose_method_tip": "There is existing data in the cloud. Please choose from where you want to keep the data. And override the other location.", - "local": "Local" + "info": "Synchronisiere Dateien mit Google Drive für Backups oder wenn du an mehreren Computern arbeitest.", + "tip_api": "du musst einen eigenen kostenlosen Google API-Schlüssel angeben, damit das Programm automatisch Dateien in dein Google Drive hochladen kann.", + "tip_how": "Du weißt nicht wie?", + "tip_guide": "Klicke hier für eine Anleitung.", + "enable": "Bei Programmstart & -ende automatisch synchronisieren", + "disable_upload": "Hochladen deaktivieren", + "media_id": "Medienpfad ID", + "google_drive_api": "Google API Service Schlüssel", + "select_key": "API-Schlüssel Datei importieren", + "update_key": "API-Schlüssel Datei ändern", + "enable_custom_folder_id": "Benutzerdefinierte Ordnernummerierung benutzen", + "main_folder": "Hauptordner manuell einstellen", + "media_folder": "Cloud Medienordner", + "reconnect": "Neu verbinden", + "sync": "Jetzt synchronisieren", + "choose_method_tip": "Es sind bereits Daten in der Cloud vorhanden. Bitte wähle entweder den Upload der lokalen Daten oder den Download aus der Cloud. Der andere Speicherort wird dabei überschrieben.", + "local": "Lokal", + "syncing": "Synchronisiere mit der Cloud", + "sync_complete": "Synchronisierung abgeschlossen" }, "export": { "export": "Exportieren", @@ -547,7 +700,6 @@ "all_shows": "Alle Shows", "all_projects": "Alle Projekte", "project": "Projekt", - "options": "Optionen", "preview": "Vorschau", "title": "Titel", "metadata": "Metadaten", @@ -555,27 +707,32 @@ "groups": "Gruppen", "numbers": "Nummerierte Folien", "invert": "Invertierte Folien", - "original_text_size": "Original text size", - "text": "Reintext", + "original_text_size": "Originale Textgröße", + "text": "reiner Text", "slides": "Folien", "rows": "Zeilen", "columns": "Spalten" }, "context": { "enabledTabs": "Tabs umschalten", + "filterByTags": "Nach Tags filtern", "addToProject": "Zu Projekt hinzufügen", + "add_to_show": "Zu Show hinzufügen", + "lockForChanges": "Für die Bearbeitung sperren", "newCategory": "Neue Kategorie", "changeIcon": "Symbol wechseln", "changeGroup": "Gruppe wechseln", "createNew": "Erstelle neu", "selectAll": "Alle auswählen", "force_outputs": "Ausgabe erzwingen", + "align_with_screen": "An Bildschirm ausrichten", "toggle_output": "Ausgabe umschalten", "move_to_front": "In den Vordergrund", - "lock_to_output": "Auf Ausgabe sperren", - "place_under_slide": "Place under slide", + "hide_from_preview": "In der Vorschau ausblenden", + "lock_to_output": "Auf Ausgabe fixieren", + "place_under_slide": "Unter Folie platzieren", "toggle_clock": "Uhr umschalten", - "move_connections": "Move all connected to this" + "move_connections": "Auf allen verbundenen Geräten verwenden" }, "tools": { "notes": "Notizen", @@ -591,30 +748,46 @@ "slide": "Folie" }, "edit": { + "text": "Text", "font": "Schriftart", - "family": "Schriftname", + "family": "Schriftart", + "font_size": "Schriftgröße", + "text_fit": "Textanpassung", + "shrink_to_fit": "Passend verkleinern", + "grow_to_fit": "Passend vergrößern", + "text_color": "Textfarbe", "style": "Style", + "lines": "Zeilen", "options": "Optionen", "_title_bold": "Fett", "_title_italic": "Kursiv", "_title_underline": "Unterstrichen", "_title_strikethrough": "Durchgestrichen", "color": "Farbe", + "accent_color": "Akzentfarbe", "background_color": "Hintergrundfarbe", - "background_opacity": "Background opacity", - "background_image": "Background image", - "media_fit": "Media fit", + "background_opacity": "Hintergrund Deckkraft", + "background_image": "Hintergrundbild", + "background_media": "Hintergrundmedien", + "overlay_content": "Überlagerungsinhalt hinzufügen", + "different_first_template": "Eigene Vorlage auf erster Folie", + "media_fit": "Medienskalierung", "size": "Größe", + "max_lines": "Maximale Zeilenanzahl", + "invert_items": "Elemente umkehren", "chords": "Akkorde", + "transpose": "Transponieren", "auto_size": "Automatische Größe", + "no_wrap": "Text in einer Zeile", "line_spacing": "Zeilenabstand", - "line_height": "Line height", + "line_height": "Zeilenhöhe", "letter_spacing": "Zeichenabstand", "word_spacing": "Wortabstand", - "transform": "Transform", - "uppercase": "Uppercase", - "lowercase": "Lowercase", - "capitalize": "Capitalize", + "transform": "Transformation", + "text_transform": "Texttransformation", + "uppercase": "Großbuchstaben", + "lowercase": "Kleinbuchstaben", + "capitalize": "Kapitalisieren", "align": "Ausrichtung", "_title_left": "Linksbündig", "_title_center": "Zentriert", @@ -623,17 +796,17 @@ "_title_top": "Oben", "_title_bottom": "Unten", "outline": "Außenlinie", - "width": "Breite", "shadow": "Schatten", "shadow_inset": "Schattenwurf", "offsetX": "Versatz X", "offsetY": "Versatz Y", "blur": "Unschärfe", "item": "Element", + "width": "Breite", "height": "Höhe", "rotation": "Rotation", - "tilt": "Tilt", - "perspective": "Perspective", + "tilt": "Neigung", + "perspective": "Perspektive", "opacity": "Transparenz", "corner_radius": "Eckenradius", "border": "Rand", @@ -644,33 +817,37 @@ "add_icons": "Symbole hinzufügen", "arrange_items": "Elemente anordnen", "filters": "Filter", - "backdrop_filters": "Backdrop filters", - "interval": "Interval", + "backdrop_filters": "Hintergrund Filter", + "interval": "Intervall", "video": "Video", "choose_media": "Medien auswählen", - "recent": "Recently edited", - "enable_stage": "Enable stage", - "select_stage": "Select stage", - "use_slide_index": "Use active index", - "slide_index": "Slide index", + "recent": "Zuletzt bearbeitet", + "enable_stage": "Bühnenansicht", + "select_stage": "Bühnenansicht auswählen", + "next_slide": "Nächste Folie", + "use_slide_index": "Aktiven Index verwenden", + "slide_index": "Folien Index", "padding": "Padding", - "special": "Special", - "scrolling": "Scrolling", - "top_bottom": "Top to bottom", - "bottom_top": "Bottom to top", - "left_right": "Left to right", - "right_left": "Right to left", - "max_events": "Max events", - "start_days_from_today": "Start at days from today", - "just_one_day": "Just one day", - "enable_start_date": "Enable start date" + "special": "Spezial", + "scrolling": "Scrollen", + "scrolling_speed": "Scrollgeschwindigkeit", + "top_bottom": "Oben nach Unten", + "bottom_top": "Unten nach Oben", + "left_right": "Links nach Rechts", + "right_left": "Rechts nach Links", + "max_events": "Maximale Anzahl", + "start_days_from_today": "Beginn in Tagen ab heute", + "just_one_day": "Nur ein Tag", + "enable_start_date": "Startdatum verwenden", + "disable_navigation": "Navigation deaktivieren", + "progress_bar": "Fortschrittsanzeige" }, "items": { "text": "Textbox", - "list": "List", + "list": "Liste", "media": "Medien", - "camera": "Camera", "image": "Bild", + "camera": "Kamera", "video": "Video", "mirror": "Spiegeln", "clock": "Uhr", @@ -680,7 +857,9 @@ "timer": "Timer", "variable": "Variable", "web": "Website", + "slide_tracker": "Fortschritt", "visualizer": "Visualizer", + "captions": "Untertitel", "icon": "Symbol" }, "borders": { @@ -695,17 +874,17 @@ }, "list": { "disc": "Disc", - "circle": "Circle", - "square": "Square", - "disclosure-closed": "Arrow", - "disclosure-open": "Triangle", - "decimal": "Number", - "decimal-leading-zero": "Number with zero", - "lower-alpha": "Letter", - "upper-alpha": "Upper letter", - "lower-roman": "Roman", - "upper-roman": "Upper roman", - "lower-greek": "Greek" + "circle": "Kreis", + "square": "Quadrat", + "disclosure-closed": "Pfeil", + "disclosure-open": "Dreieck", + "decimal": "Zahl", + "decimal-leading-zero": "Zahl mit Null", + "lower-alpha": "Buchstabe", + "upper-alpha": "Großbuchstabe", + "lower-roman": "Römisch", + "upper-roman": "Römisch großgeschrieben", + "lower-greek": "Griechisch" }, "timer": { "from_to": "Von Start zum Ende", @@ -716,16 +895,16 @@ "seconds": "Sekunden", "from": "Von", "to": "Bis", - "overflow": "Overflow when reached end", - "overflow_color": "Overflow color", + "overflow": "Nach Ende weiterlaufen", + "overflow_color": "Überlauf Farbe", "preview": "Vorschau", "clock": "Uhr", "event": "Veranstaltung", "no_events": "Es gibt keine geplanten Veranstaltungen", "edit": "Bearbeiten", "create": "Erstellen", - "line": "Line", - "mask": "Mask" + "line": "Linie", + "mask": "Maske" }, "clock": { "type": "Typ", @@ -733,24 +912,41 @@ "analog": "Analog", "seconds": "Sekunden" }, + "captions": { + "info": "Klicke auf die URL, um sie im Browser zu öffnen, falls du das nicht schon getan hast, oder öffne sie auf einem anderen Gerät! Stelle sicher, dass du Zugriff auf das Mikrofon erlaubst.", + "language": "Transkript Sprache", + "translate": "Übersetzen in", + "showtime": "Anzeigedauer", + "powered_by": "Powered by" + }, + "localization": { + "translate": "Übersetzen", + "add": "Übersetzung hinzufügen", + "update": "Übersetzung aktualisieren", + "remove": "Übersetze Elemente entfernen" + }, "midi": { + "midi": "MIDI", + "activate": "Durch MIDI-Signal aktivieren", + "activate_keypress": "Durch Tastendruck aktivieren", "name": "Name", - "input": "Input", - "output": "Output", - "type": "Type", + "input": "Eingang", + "output": "Ausgang", + "type": "Typ", "note": "Note", "velocity": "Velocity", - "channel": "Channel", - "start_action": "Start action", - "use_default_values": "Use default values", - "auto_values": "Update values when receiving a MIDI input", - "tip_velocity": "Set velocity to -1 to disable.", - "tip_action": "To activate spesific slides, right click any slide and choose the midi in action.", - "tip_index_by_velocity": "Index is determined by the received velocity, starting at 0." + "channel": "Kanal", + "start_action": "Aktion starten", + "use_default_values": "Standardwerte verwenden", + "auto_values": "Werte aktualisieren wenn ein MIDI Signal empfangen wird", + "tip_velocity": "Zum Deaktivieren Velocity auf -1 setzen.", + "tip_action": "Um bestimmte Folien zu aktivieren, rechtsklicke auf eine Folie und wähle die MIDI Folien-Aktion", + "tip_index_by_velocity": "Der Index wird durch die empfangene Velocity bestimmt, beginnend bei 0." }, "draw": { "focus": "Fokus", "pointer": "Pointer", + "zoom": "Zoom", "fill": "Ausfüllen", "paint": "Malen", "particles": "Partikel", @@ -767,25 +963,31 @@ }, "stage": { "slide": "Folie", + "stage_layout": "Layout Bühnenansicht", "current_slide_text": "Text der aktuellen Folie", "current_slide": "Aktuelle Folie", "current_slide_notes": "Notizen zur aktuellen Folie", "next_slide_text": "Text der nächsten Folie", "next_slide": "Nächste Folie", "next_slide_notes": "Notizen zur nächsten Folie", - "output": "Output", - "current_output": "Current output", + "output": "Ausgabe", + "current_output": "Aktuelle Ausgabe", + "slide_tracker": "Fortschritt", "time": "Uhrzeit", "system_clock": "Systemuhr", "video_time": "Videozeit", - "video_countdown": "Video Countdown", + "video_countdown": "Video-Countdown", + "first_active_timer": "Erster aktiver Timer", "other": "Andere", "message": "Mitteilung", "color": "Farbe", "font-size": "Schriftgröße", "zeros": "Nullen", "overrun": "Überlauf Farbe", - "auto_stretch": "Auto stretch content" + "source_output": "Quell-Ausgabe", + "auto_stretch": "Inhalt automatisch strecken", + "labels": "Beschriftung anzeigen", + "label_color": "Beschriftungsfarbe" }, "settings": { "general": "Allgemein", @@ -793,64 +995,85 @@ "groups": "Gruppen", "styles": "Styles", "display_settings": "Ausgaben", - "actions": "Aktionen", "display": "Anzeige", "connection": "Verbindung", "cloud": "Cloud", - "calendar": "Kalendar", + "calendar": "Kalender", + "text_import": "Text", + "media_import": "Medien", "other": "Andere", "language": "Sprache", - "autosave": "Autosave", - "never": "Never", - "minutes": "minutes", + "autosave": "Automatisches Speichern", + "never": "Nie", + "minutes": "Minuten", "use24hClock": "24h-Format verwenden", - "styles_hint": "Create different styles that can be applied to the output to change the look.", - "hide_output_hint": "Doppelklick auf das Ausgabefenster, um es zu verstecken. Hold CTRL to enable dragging.", - "hide_menubar_hint": "To hide the menu bar, enable kiosk mode, or enable \"Automatically hide or show the menu bar\" in the general MacOS settings.", + "styles_hint": "Erstelle unterschiedliche Stile, die auf den Ausgaben angewendet werden können, um ihr Aussehen zu verändern.", + "hide_output_hint": "Doppelklick auf das Ausgabefenster, um es zu verstecken. Zum Verschieben CTRL gedrückt halten.", + "hide_menubar_hint": "Um die Menüleiste auszublenden, aktiviere den Kioskmodus oder aktiviere „Menüleiste automatisch ein- und ausblenden“ in den allgemeinen MacOS-Einstellungen.", "show_output_hint": "Halte Strg/Cmd gedrückt, während du auf die Anzeige-Schaltfläche klickst, um die Anzeige über diesen Monitor zu erzwingen.", "move_output_hint": "Findest du deine Anzeige nicht? Ändere die Position, bis das Fenster auf dem zweiten Bildschirm angezeigt wird.", - "select_display": "Click on the screen where you wan't to display the output window.", - "manual_input_hint": "Can't find the display? Click here to manually change the position.", - "manual_drag_hint": "You can also hold ctrl/cmd over an active output window to manually drag it around.", + "select_display": "Klicke auf den Bildschirm, auf dem das Ausgabefenster angezeigt werden soll.", + "manual_input_hint": "Der Bildschirm wird nicht angezeigt? Hier klicken, um die Position manuell einzugeben.", + "manual_drag_hint": "Du kannst auch Ctrl/Cmd über einem aktiven Ausgabefenster gedrückt halten, um es manuell zu verschieben.", + "allow_main_screen": "Ausgabe auf Hauptbildschirm zulassen", + "identify_screens": "Bildschirme identifizieren", "new_output": "Neue Ausgabe", - "enable_key_output": "Enable alpha key output", - "always_on_top": "Always on top", - "kiosk_mode": "Kiosk mode", - "change_key_output_position": "Change key output position", + "normal": "Normal", + "enable_key_output": "Alpha-Key-Ausgabe aktivieren", + "always_on_top": "Immer im Vordergrund", + "kiosk_mode": "Kioskmodus", + "change_key_output_position": "Key Ausgabeposition ändern", "position": "Position", "enabled": "Aktiviert", "color_when_active": "Farbe wenn aktiv", "fixed": "Fixiert", "lines": "Zeilen", - "override_with_template": "Überschreiben des Stils mit Vorlage", + "override_with_template": "Folienstil mit Vorlage überschreiben", + "override_scripture_with_template": "Bibelstellenstil mit Vorlage überschreiben", "active_layers": "Aktive Ebenen", "window": "Fenster", - "active_style": "Use style", - "alert_updates": "Hinweis, wenn ein neues Update verfügbar ist", + "active_style": "Style anwenden", + "alert_updates": "Hinweisen, wenn ein neues Update verfügbar ist", + "auto_updates": "Automatische Updates", "disable_labels": "Beschriftungen deaktivieren", "group_numbers": "Gruppennummerierung", - "full_colors": "Folien Gruppenfarbe", + "full_colors": "Gruppenfarben mit hohem Kontrast", + "slide_number_keys": "Folien über den Ziffernblock abspielen", "auto_output": "Zeige Ausgabebildschirm beim Start", + "hide_cursor_in_output": "Cursor in der Ausgabe ausblenden", + "clear_media_when_finished": "Medien nach der Wiedergabe freigeben", + "disable_presenter_controller_keys": "Presenter Controller Tasten deaktivieren", "default_project_name": "Standard Projektname", + "audio_fade_duration": "Dauer der Audio Überblendung", + "audio_crossfade": "Audio Überblendung", + "clear_style_background_on_text": "Hintergrundstil entfernen wenn die Folie aktiv ist", "resolution": "Auflösung", "cropping": "Cropping", - "frame_rate": "Frame rate", + "frame_rate": "Framerate", + "device": "Gerät", + "display_mode": "Anzeigemodus", + "pixel_format": "Pixelformat", + "alpha_key": "Alpha-Key", "transparent": "Transparent", + "invisible_window": "Nicht sichtbares Fenster", "video_extensions": "Video-Erweiterungen", "image_extensions": "Bild-Erweiterungen", "add": "Hinzufügen", "remove": "Entfernen", "change_name": "Name ändern", - "show_location": "Ort anzeigen", - "export_location": "Export location", - "scripture_location": "Scripture location", - "recording_location": "Recordings location", + "show_location": "Show Speicherort", + "data_location": "Daten Speicherort", + "user_data_location": "Nutzereinstellungen in \"Datenspeicherort\" speichern", + "popup_before_close": "Vor dem Beenden immer nachfragen", + "disable_hardware_acceleration": "Hardwarebeschleunigung deaktivieren", + "restart_for_change": "Änderung ist erst nach einem Neustart des Programms wirksam!", "font": "Schrift", "font_family": "Schriftart", "font_size": "Schriftgröße", + "border_radius": "Eckenradius", "colors": "Farben", "add_group": "Gruppe hinzufügen", - "group_shortcut": "Shortcut to activate group", + "group_shortcut": "Tastaturkürzel um Gruppe zu aktivieren", "output_screen": "Ausgabebildschirm", "device_name": "Gerätename", "password": "Passwort", @@ -859,40 +1082,43 @@ "max_connections": "Max Verbindungen", "allowed_connections": "Zugelassene Verbindungen", "connect": "Verbinden, indem das in den Webbrowser eingegeben wird", - "connect_qr": "Or scan this QR code", + "connect_qr": "Oder scanne den QR-Code", "restart": "Server neustarten", "reset_all": "Alles zurücksetzen", "reset_theme": "Design zurücksetzen", "reset_themes": "Alle Designs zurücksetzen", - "backup_all": "Backup everything", - "restore": "Restore", - "backup_started": "Backing up...", - "restore_started": "Restoring...", - "backup_finished": "Backup complete!", - "restore_finished": "Restore complete!", - "preview_frame_rate": "Preview frame rate", + "capitalize_words": "Wörter großschreiben", + "comma_seperated": "Durch Komma getrennt", + "backup_all": "Backup erstellen", + "restore": "Backup wiederherstellen", + "backup_started": "Backup wird erstellt...", + "restore_started": "Backup wird geladen...", + "backup_finished": "Backup erstellt!", + "restore_finished": "Backup geladen!", + "preview_frame_rate": "Vorschau Framerate", "auto": "Auto", - "optimized": "Optimized", - "full": "Full" + "optimized": "Optimiert", + "reduced": "Reduziert", + "full": "Voll" }, "sort": { - "sort_by": "Sort by", + "sort_by": "Sortieren nach", "name": "Name", - "date": "Date", + "date": "Datum", "size": "Größe", "type": "Typ", - "custom": "Custom" + "custom": "Benutzerdefiniert" }, "calendar": { "type": "Typ", "event": "Veranstaltung", - "show": "Show planen", + "schedule_action": "Aktion planen", "name": "Name", "color": "Farbe", "time": "Uhrzeit", - "from_date": "Von Datum", + "from_date": "Ab Datum", "to_date": "Bis Datum", - "from_time": "Von Uhrzeit", + "from_time": "Ab Uhrzeit", "to_time": "Bis Uhrzeit", "location": "Ort", "notes": "Notizen", @@ -905,20 +1131,25 @@ "week": "Woche", "month": "Monat", "year": "Jahr", - "ending_the": "den", - "ending_repeated": "wiederholt", + "ending_the": "zum", + "ending_repeated": "wiederhole", "ending_times": "mal", "save_all": "Speichern und alle aktualisieren", - "add_slides_from_show": "Include slides from show" + "add_slides_from_show": "Folien von Show einfügen" }, "scripture": { "bibles": "Bibel von API.Bible", "custom": "Oder importiere deine eigene", "max_verses": "Max Verse pro Folie", "verse_numbers": "Versnummern", + "verses_on_individual_lines": "Verse in separaten Zeilen", "version": "Zeige Übersetzung", "reference": "Zeige Quelle", - "red_jesus": "Jesuworte in rot" + "split_reference": "Quelle aufteilen", + "combine_with_text": "Mit Text kombinieren", + "reference_at_bottom": "Nach unten", + "red_jesus": "Jesuworte in rot", + "search": "In der Bibel suchen" }, "filter": { "blur": "Unschärfe", @@ -935,17 +1166,35 @@ "screens": "Bildschirme", "windows": "Fenster", "cameras": "Kameras", - "microphones": "Mikrofone" + "microphones": "Mikrofone", + "audio_streams": "Audio Streams" + }, + "presentation_control": { + "unsupported": "Presentation-Controller wird nicht unterstützt auf ", + "unsupported_tip": "Sie können diese in Bilder konvertieren und eine neue Show erstellen, oder alternativ 'Immer im Vordergrund' deaktivieren, um aus jedem anderen Programm heraus zu präsentieren.", + "opening": "Wird geöffnet! Bitte warten...", + "retry": "Bitte versuchen manuell zu öffnen, oder", + "try_again": "erneut versuchen", + "restart": "Neu starten", + "start": "Starten", + "choose_window": "Bitte ein Fenster auswählen" }, "transition": { "current_slide": "für aktuelle Folie", "text": "Textübergang", "media": "Medienübergang", - "duration": "Duration", + "slide_transition": "Folienübergang", + "background_transition": "Hintergrundübergang", + "specific": "Spezifischere Übergänge aktivieren", + "between": "Zwischen", + "in": "In", + "out": "Out", + "duration": "Dauer", "easing": "Easing", + "direction": "Richtung", "type": "Typ", "none": "Kein", - "fade": "Ausblenden", + "fade": "Fade", "blur": "Verwischen", "scale": "Skalieren", "spin": "Drehen", @@ -965,10 +1214,10 @@ "sine": "Sinusförmig" }, "variables": { - "number": "Number", + "number": "Zahl", "text": "Text", - "step": "Step", - "value": "Value" + "step": "Schrittgröße", + "value": "Wert" }, "month": { "1": "Januar", @@ -1003,7 +1252,11 @@ "slides": "Folien", "words": "Wörter", "template": "Vorlage", - "category": "Kategorie" + "category": "Kategorie", + "photoUrl": "Link zum Foto", + "likes": "Gefällt mir", + "artist": "Künstler", + "artistUrl": "Künstlerseite" }, "songbeamer_import": { "options": "Optionen", @@ -1018,6 +1271,6 @@ "translation_layouts": "Layouts", "translation_description_multiline": "Fügt alle Sprachen in eine einzelne Textbox in abwechselnden Zeilen ein. (Wie in Songbeamer)", "translation_description_textboxes": "Fügt jede Sprache in eine separate Textbox auf der Folie ein.", - "translation_description_layouts": "Erstellt für jede Sprache eigene Slides und ein eigenes Layout nur für diese Sprache." + "translation_description_layouts": "Erstellt für jede Sprache eigene Folien und ein eigenes Layout nur für diese Sprache." } } diff --git a/public/lang/en.json b/public/lang/en.json index f30e0b8d..f106ce5b 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -102,7 +102,8 @@ "quick_lyrics_tip": "Paste text or enter manually", "quick_lyrics_example_tip": "Enter lyrics or any text here", "quick_lyrics_example_text": "Line", - "empty": "Empty show" + "empty": "Empty show", + "exists": "Found an existing show with the same name" }, "preview": { "_previous_show": "Previous show", @@ -381,6 +382,7 @@ "transition": "Transition", "delete_show": "Delete show", "delete_show_confirmation": "Are you sure you want to delete", + "delete_duplicated_shows": "Delete duplicated shows", "change_name": "Change name on", "choose_screen": "Choose screen", "choose_output": "Choose output type", @@ -485,7 +487,11 @@ "slide_template": "Slide template", "source": "Source", "artist": "Artist", - "song": "Song" + "song": "Song", + "delete_manual": "Manual check", + "delete_match": "Delete if exact match", + "delete_keep_last_modified": "Delete all except last modified", + "delete_keep_first_created": "Delete all except first created" }, "actions": { "rename": "Rename", @@ -594,7 +600,10 @@ "play_on_midi_tip": "Activate this specific slide when receiving chosen MIDI signal", "send_midi": "Send MIDI signal", "delete_shows_not_indexed": "Delete shows in 'Shows' folder that are not indexed", + "delete_empty_shows": "Delete empty shows", "delete_thumbnail_cache": "Delete thumbnail cache", + "export_usage_log": "Export usage log", + "reset_usage_log": "Reset usage log", "open_log_file": "Open log file", "open_cache_folder": "Open cache folder", "refresh_all_shows": "Get all shows in 'Shows' folder", @@ -644,6 +653,8 @@ "activate_slide_clicked": "Activate on slide click", "activate_video_starting": "Activate when video is starting", "activate_video_ending": "Activate when video is ending", + "activate_audio_starting": "Activate when audio is starting", + "activate_audio_ending": "Activate when audio is ending", "activate_timer_ending": "Activate when timer is ending", "activate_scripture_start": "Activate when scripture is started", "activate_slide_cleared": "Activate when slide is cleared", @@ -1064,7 +1075,7 @@ "show_location": "Show location", "data_location": "Data location", "user_data_location": "Save user settings at 'Data location'", - "popup_before_close": "Always display popup before closing", + "popup_before_close": "Enable close confirmation popup", "disable_hardware_acceleration": "Disable hardware acceleration", "restart_for_change": "You have to restart the program for the change to take effect!", "font": "Font", @@ -1095,6 +1106,7 @@ "restore_started": "Restoring...", "backup_finished": "Backup complete!", "restore_finished": "Restore complete!", + "auto_backup": "Auto backup", "preview_frame_rate": "Preview frame rate", "auto": "Auto", "optimized": "Optimized", @@ -1242,6 +1254,11 @@ "6": "Saturday", "7": "Sunday" }, + "interval": { + "daily": "Daily", + "weekly": "Weekly", + "mothly": "Monthly" + }, "info": { "created": "Created", "modified": "Modified", diff --git a/public/lang/hu.json b/public/lang/hu.json index 2aa31bfb..d43f3e73 100644 --- a/public/lang/hu.json +++ b/public/lang/hu.json @@ -49,7 +49,7 @@ }, "tooltip": { "project": "Új projekt létrehozása, ahol bemutatókat adhat hozzá és elrendezheti azokat.", - "show": "Új bemutató létrehozása, ahol hozzáadhat dalszövegeket, prezentációkat és médiaanyagokat. Tartsa lenyomva a Ctrl/Cmd billentyűt üres műsor létrehozásához.", + "show": "Új bemutató létrehozása, ahol hozzáadhat dalszövegeket, prezentációkat és médiaanyagokat.", "groups": "Az összes csoport a jelenlegi bemutatóban és az összes globális csoport. Kattintson rájuk vagy húzza őket a jelenlegi elrendezéshez.", "layout": "Átmenetek és következő időzítők hozzáadása a jelenlegi elrendezésben található diákhoz.", "media": "Az összes média a jelenlegi bemutatóban. Tegye ki élőbe, vagy húzza őket az aktuális elrendezéshez.", @@ -90,13 +90,19 @@ "bold": "Félkövér" }, "create_show": { + "web": "Webes keresés", "search_web": "Dal keresése a weben", + "search_results": "Keresési eredmények", "more_options": "További lehetőségek", "format_new_show": "Szövegformázás", "format_new_show_tip": "A szövegformátum fejlesztése automatikus nagybetűzéssel, a szöveg felosztásával, csoportok hozzárendelésével és még sok mással.", "split_lines": "Sorok száma", "split_lines_tip": "Diánként engedélyezett sorok száma az automatikus felosztás előtt", - "quick_lyrics_example_text": "Sor" + "quick_lyrics": "Gyors dalszöveg", + "quick_lyrics_tip": "Illessze be a vágólapról vagy gépeljen", + "quick_lyrics_example_tip": "Adja meg a dalszöveget vagy bármilyen szöveget itt", + "quick_lyrics_example_text": "Sor", + "empty": "Üres bemutató" }, "preview": { "_previous_show": "Előző bemutató", @@ -199,7 +205,8 @@ "settings": "Beállítások", "_title_settings": "Beállítások", "_title_display": "Prezentálás", - "_title_display_stop": "Prezentáció leállítása" + "_title_display_stop": "Prezentáció leállítása", + "again_confirm": "Kattintás a megerősítéshez" }, "empty": { "general": "Itt nincs semmi", @@ -263,7 +270,7 @@ "message": "Üzenet", "message_tip": "Valamit jelenítsen meg az összes dián", "auto_media": "Metaadatok lekérése a média tartalmából", - "override_output": "Stílus felülírása a kimeneten", + "override_output": "Kimeneti stílus felülírása", "display_metadata": "Metaadatok megjelenítése", "meta_template": "Metaadat sablon", "text_divider": "Szövegelválasztó", @@ -466,7 +473,6 @@ "show": { "name": "Név", "category": "Kategória", - "quick_lyrics": "Gyors dalszöveg", "new_layout": "Új elrendezés", "grid": "Rács nézet", "simple": "Egyszerű nézet", @@ -477,7 +483,6 @@ "locked": "Ez a bemutató zárolva van!", "locked_info": "Ez a bemutató zárolva lett és nem lehet szerkeszteni. Oldja fel a bemutató rajzolóban. ", "slide_template": "Diasablon", - "search_results": "Keresési eredmény", "source": "Forrás", "artist": "Művész", "song": "Dal" @@ -646,6 +651,13 @@ "activate_show_created": "Aktiválás bemutató létrehozásakor", "activate_audio_playlist_ended": "Aktiválás hanglejátszási lista végén" }, + "recording": { + "remove": "Felvétel eltávolítása", + "tip": "Diák időzítésének rögzítése és lejátszása . Szinkronizálás az első dia hangsávjával.", + "layout_changed": "Az elrendezés módosult az utolsó rögzítés óta!", + "audio_synced": "Hanggal szinkronizálva!", + "start": "Diarögzítés indítása" + }, "animate": { "change": "Módosítás", "set": "Beállítás", @@ -688,7 +700,6 @@ "all_shows": "Összes bemutató", "all_projects": "Összes projekt", "project": "Projekt", - "options": "Beállítások", "preview": "Előnézet", "title": "Cím", "metadata": "Metaadatok", @@ -763,6 +774,7 @@ "media_fit": "Média méretezése", "size": "Méret", "max_lines": "Maximum sor", + "invert_items": "Elemek megfordítása", "chords": "Akkordok", "transpose": "Transzponálás", "auto_size": "Automatikus méret", @@ -974,7 +986,8 @@ "overrun": "Túlcsordulás színe", "source_output": "Forráskimenet", "auto_stretch": "Tartalom automatikus nyújtása", - "labels": "Címkék megjelenítése" + "labels": "Címkék megjelenítése", + "label_color": "Színek címkézése" }, "settings": { "general": "Általános", diff --git a/src/electron/capture/helpers/CaptureTransmitter.ts b/src/electron/capture/helpers/CaptureTransmitter.ts index e424a1ca..2ba3f32a 100644 --- a/src/electron/capture/helpers/CaptureTransmitter.ts +++ b/src/electron/capture/helpers/CaptureTransmitter.ts @@ -105,6 +105,7 @@ export class CaptureTransmitter { if (channel.imageIsSame) return const size = image.getSize() + if (!size.width || !size.height) return switch (key) { //case "preview": diff --git a/src/electron/cloud/drive.ts b/src/electron/cloud/drive.ts index 97f2a5cf..f786e898 100644 --- a/src/electron/cloud/drive.ts +++ b/src/electron/cloud/drive.ts @@ -4,7 +4,7 @@ import { isProd, toApp } from ".." import { STORE } from "../../types/Channels" import { stores } from "../data/store" import { checkShowsFolder, dataFolderNames, deleteFile, doesPathExist, getDataFolder, getFileStats, loadShows, readFileAsync, writeFile } from "../utils/files" -import { trimShow } from "../utils/responses" +import { trimShow } from "../utils/shows" let driveClient: any = null const DEBUG = !isProd diff --git a/src/electron/data/backup.ts b/src/electron/data/backup.ts index 479d280a..ffcc7235 100644 --- a/src/electron/data/backup.ts +++ b/src/electron/data/backup.ts @@ -1,19 +1,20 @@ import path from "path" import { toApp } from ".." import { MAIN, STORE } from "../../types/Channels" -import { dataFolderNames, doesPathExist, getDataFolder, makeDir, openSystemFolder, readFile, selectFilesDialog, writeFile } from "../utils/files" -import { stores } from "./store" +import { createFolder, dataFolderNames, doesPathExist, getDataFolder, getTimePointString, makeDir, openSystemFolder, readFile, selectFilesDialog, writeFile } from "../utils/files" +import { stores, updateDataPath } from "./store" // "SYNCED_SETTINGS" and "STAGE_SHOWS" has to be before "SETTINGS" and "SHOWS" const storesToSave = ["SYNCED_SETTINGS", "STAGE_SHOWS", "SHOWS", "EVENTS", "OVERLAYS", "PROJECTS", "SETTINGS", "TEMPLATES", "THEMES", "MEDIA", "DRIVE_API_KEY"] // don't upload: config.json, cache.json, history.json -export async function startBackup({ showsPath, dataPath, scripturePath }: any) { +export async function startBackup({ showsPath, dataPath, scripturePath, customTriggers }: any) { let shows: any = null // let bibles: any = null console.log(scripturePath) let backupPath: string = getDataFolder(dataPath, dataFolderNames.backups) + let backupFolder = createFolder(path.join(backupPath, getTimePointString())) // CONFIGS await Promise.all(storesToSave.map(syncStores)) @@ -25,8 +26,10 @@ export async function startBackup({ showsPath, dataPath, scripturePath }: any) { // SHOWS await syncAllShows() - toApp(MAIN, { channel: "BACKUP", data: { finished: true, path: backupPath } }) - openSystemFolder(backupPath) + toApp(MAIN, { channel: "BACKUP", data: { finished: true, path: backupFolder } }) + + if (customTriggers?.changeUserData) updateDataPath(customTriggers.changeUserData) + else if (!customTriggers?.silent) openSystemFolder(backupFolder) return @@ -40,7 +43,7 @@ export async function startBackup({ showsPath, dataPath, scripturePath }: any) { // else if (id === "SYNCED_SETTINGS") bibles = store.store?.scriptures let content: string = JSON.stringify(store.store) - let p: string = path.resolve(backupPath, name) + let p: string = path.resolve(backupFolder, name) writeFile(p, content) } @@ -60,7 +63,7 @@ export async function startBackup({ showsPath, dataPath, scripturePath }: any) { } let content: string = JSON.stringify(allShows) - let p: string = path.resolve(backupPath, name) + let p: string = path.resolve(backupFolder, name) writeFile(p, content) } } diff --git a/src/electron/data/export.ts b/src/electron/data/export.ts index 04bcf153..6f56510f 100644 --- a/src/electron/data/export.ts +++ b/src/electron/data/export.ts @@ -7,10 +7,10 @@ import fs from "fs" import { join } from "path" import { EXPORT, MAIN, STARTUP } from "../../types/Channels" import { isProd, toApp } from "../index" -import { dataFolderNames, doesPathExist, getDataFolder, getShowsFromIds, makeDir, openSystemFolder, parseShow, readFile, selectFolderDialog } from "../utils/files" +import { createFolder, dataFolderNames, doesPathExist, getDataFolder, getShowsFromIds, getTimePointString, makeDir, openSystemFolder, parseShow, readFile, selectFolderDialog } from "../utils/files" import { exportOptions } from "../utils/windowOptions" import { Message } from "../../types/Socket" -import { getAllShows } from "../utils/responses" +import { getAllShows } from "../utils/shows" // SHOW: .show, PROJECT: .project, BIBLE: .fsb const customJSONExtensions: any = { @@ -36,6 +36,12 @@ export function startExport(_e: any, msg: Message) { return } + if (msg.channel === "USAGE") { + let path = createFolder(join(msg.data.path, "Usage")) + exportJSONFile(msg.data.content, path, getTimePointString()) + return + } + if (msg.channel === "ALL_SHOWS") { exportAllShows(msg.data) return @@ -127,6 +133,10 @@ export function exportJSON(content: any, extension: string, path: string) { writeFile(join(path, content.name || "Unnamed"), extension, JSON.stringify(content, null, 4), "utf-8", (err: any) => doneWritingFile(err, path)) } +export function exportJSONFile(content: any, path: string, name: string) { + writeFile(join(path, name), ".json", JSON.stringify(content, null, 4), "utf-8", (err: any) => doneWritingFile(err, path)) +} + // ----- SHOW ----- export function exportShow(data: any) { @@ -212,10 +222,7 @@ function exportAllShows(data: any) { if (shows.length) { // create custom folder to organize the amount of files - let folderName = new Date().toISOString() - folderName = folderName.slice(0, folderName.indexOf("T")) - folderName += `_${new Date().getHours()}-${new Date().getMinutes()}` - data.path = join(data.path, folderName) + data.path = join(data.path, getTimePointString()) makeDir(data.path) if (type === "show") exportShow({ ...data, shows }) diff --git a/src/electron/data/store.ts b/src/electron/data/store.ts index c9639244..c5b53e22 100644 --- a/src/electron/data/store.ts +++ b/src/electron/data/store.ts @@ -25,6 +25,7 @@ const fileNames: { [key: string]: string } = { media: "media", cache: "cache", history: "history", + usage: "usage", } // NOTE: defaults will always replace the keys with any in the default when they are removed @@ -96,6 +97,7 @@ let driveKeys = new Store({ name: fileNames.driveKeys, defaults: {}, ...storeExt const media = new Store({ name: fileNames.media, defaults: {}, accessPropertiesByDotNotation: false, serialize: (v) => JSON.stringify(v), ...storeExtraConfig }) const cache = new Store({ name: fileNames.cache, defaults: {}, serialize: (v) => JSON.stringify(v), ...storeExtraConfig }) let history = new Store({ name: fileNames.history, defaults: {}, serialize: (v) => JSON.stringify(v), ...storeExtraConfig }) +let usage = new Store({ name: fileNames.usage, defaults: { all: [] }, serialize: (v) => JSON.stringify(v), ...storeExtraConfig }) export let stores: { [key: string]: Store } = { SETTINGS: settings, @@ -111,6 +113,7 @@ export let stores: { [key: string]: Store } = { MEDIA: media, CACHE: cache, HISTORY: history, + USAGE: usage, } // ----- GET STORE ----- diff --git a/src/electron/index.ts b/src/electron/index.ts index 93c7e37e..36b4e806 100644 --- a/src/electron/index.ts +++ b/src/electron/index.ts @@ -18,8 +18,9 @@ import { stopApiListener } from "./utils/api" import { checkShowsFolder, dataFolderNames, deleteFile, getDataFolder, loadShows, writeFile } from "./utils/files" import { template } from "./utils/menuTemplate" import { stopMidi } from "./utils/midi" -import { catchErrors, loadScripture, loadShow, receiveMain, renameShows, saveRecording, startImport } from "./utils/responses" +import { catchErrors, loadScripture, loadShow, receiveMain, saveRecording, startImport } from "./utils/responses" import { loadingOptions, mainOptions } from "./utils/windowOptions" +import { renameShows } from "./utils/shows" // ----- STARTUP ----- @@ -329,8 +330,8 @@ app.on("web-contents-created", (_e, contents) => { ipcMain.on(STORE, (e, msg) => { if (userDataPath === null) updateDataPath() - if (msg.channel === "UPDATE_PATH") updateDataPath(msg.data) - else if (msg.channel === "SAVE") save(msg.data) + // if (msg.channel === "UPDATE_PATH") updateDataPath(msg.data) + if (msg.channel === "SAVE") save(msg.data) else if (msg.channel === "SHOWS") loadShows(msg.data) else if (stores[msg.channel]) getStore(msg.channel, e) }) @@ -388,11 +389,11 @@ function save(data: any) { // SAVED if (!data.reset) { setTimeout(() => { - toApp(STORE, { channel: "SAVE", data: { closeWhenFinished: data.closeWhenFinished, backup: data.backup } }) + toApp(STORE, { channel: "SAVE", data: { closeWhenFinished: data.closeWhenFinished, customTriggers: data.customTriggers } }) }, 300) } - if (data.backup) startBackup({ showsPath: data.path, dataPath: data.dataPath, scripturePath }) + if (data.customTriggers?.backup || data.customTriggers?.changeUserData) startBackup({ showsPath: data.path, dataPath: data.dataPath, scripturePath, customTriggers: data.customTriggers }) }, 700) } diff --git a/src/electron/utils/LyricSearch.ts b/src/electron/utils/LyricSearch.ts index 5c12c3db..88ab3443 100644 --- a/src/electron/utils/LyricSearch.ts +++ b/src/electron/utils/LyricSearch.ts @@ -1,55 +1,48 @@ import axios from "axios" export type LyricSearchResult = { - source: string, - key: string, - artist: string, + source: string + key: string + artist: string title: string originalQuery?: string } export class LyricSearch { - - - - static search = async (artist:string, title: string) => { - const results = await Promise.all([ - LyricSearch.searchGenius(artist, title), - LyricSearch.searchHymnary(title) - ]) + static search = async (artist: string, title: string) => { + const results = await Promise.all([LyricSearch.searchGenius(artist, title), LyricSearch.searchHymnary(title)]) return results.flat() } - static get(song:LyricSearchResult) { + static get(song: LyricSearchResult) { if (song.source === "Genius") return LyricSearch.getGenius(song) else if (song.source === "Hymnary") return LyricSearch.getHymnary(song) return Promise.resolve("") } - //GENIUS private static getGeniusClient = () => { const Genius = require("genius-lyrics") return new Genius.Client() } - private static searchGenius = async (artist:string, title: string) => { + private static searchGenius = async (artist: string, title: string) => { try { const client = this.getGeniusClient() const songs = await client.songs.search(title + artist) - if (songs.length>3) songs.splice(3, songs.length-3) - return songs.map((s:any) => LyricSearch.convertGenuisToResult(s, title + artist)); + if (songs.length > 3) songs.splice(3, songs.length - 3) + return songs.map((s: any) => LyricSearch.convertGenuisToResult(s, title + artist)) } catch (ex) { - console.log(ex); + console.log(ex) return [] } } //Would greatly prefer to just load via url or id, but the api fails often with these methods (malformed json) - private static getGenius = async (song:LyricSearchResult) => { + private static getGenius = async (song: LyricSearchResult) => { const client = this.getGeniusClient() const songs = await client.songs.search(song.originalQuery || "") - let result = ""; + let result = "" for (let i = 0; i < songs.length; i++) { if (songs[i].id.toString() === song.key) { result = await songs[i].lyrics() @@ -59,13 +52,13 @@ export class LyricSearch { return result } - private static convertGenuisToResult = (geniusResult:any, originalQuery:string) => { + private static convertGenuisToResult = (geniusResult: any, originalQuery: string) => { return { source: "Genius", key: geniusResult.id.toString(), artist: geniusResult.artist.name, title: geniusResult.title, - originalQuery: originalQuery + originalQuery: originalQuery, } as LyricSearchResult } @@ -76,51 +69,52 @@ export class LyricSearch { const response = await axios.get(url) const csv = await response.data const songs = LyricSearch.CSVToArray(csv, ",") - if (songs.length>0) songs.splice(0, 1) - for (let i=songs.length-1; i>=0; i--) if (songs[i].length<7) songs.splice(i, 1) - if (songs.length>3) songs.splice(3, songs.length-3) - return songs.map((s:any) => LyricSearch.convertHymnaryToResult(s, title)); + if (songs.length > 0) songs.splice(0, 1) + for (let i = songs.length - 1; i >= 0; i--) if (songs[i].length < 7) songs.splice(i, 1) + if (songs.length > 3) songs.splice(3, songs.length - 3) + return songs.map((s: any) => LyricSearch.convertHymnaryToResult(s, title)) } catch (ex) { - console.log(ex); + console.log(ex) return [] } } - private static getHymnary = async (song:LyricSearchResult) => { + private static getHymnary = async (song: LyricSearchResult) => { const url = `https://hymnary.org/text/${song.key}` const response = await axios.get(url) const html = await response.data - const regex = /
(.*?)<\/div>/sg + const regex = /
(.*?)<\/div>/gs const match = regex.exec(html) let result = "" if (match) { result = match[0] + result = result.replace(//gi, "\n") + result = result.replaceAll("\n\n", "\n").replaceAll("\n\r\n", "\n") result = result.replaceAll("

", "\n\n") - result = result.replace(/<[^>]*>?/gm, ''); + result = result.replaceAll("\n\n\n", "\n\n").replaceAll("\n\r\n\n", "\n\n") + result = result.replace(/<[^>]*>?/gm, "") const lines = result.split("\n") - const newLines:any[] = [] - lines.forEach((line, idx) => { - if (idx { + let contents = line.replace(/^\d+\s+/gm, "").trim() //remove leading numbers + newLines.push(contents) + }) + result = newLines.join("\n").trim() } return result } - private static convertHymnaryToResult = (hymnaryResult:any, originalQuery:string) => { + private static convertHymnaryToResult = (hymnaryResult: any, originalQuery: string) => { return { source: "Hymnary", key: hymnaryResult[4], artist: hymnaryResult[6], title: hymnaryResult[0], - originalQuery: originalQuery + originalQuery: originalQuery, } as LyricSearchResult } @@ -128,26 +122,23 @@ export class LyricSearch { // This will parse a delimited string into an array of // arrays. The default delimiter is the comma, but this // can be overriden in the second argument. - static CSVToArray( strData:string, strDelimiter:string ){ - strDelimiter = (strDelimiter || ","); - - var objPattern = new RegExp(( - "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" + - "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + - "([^\"\\" + strDelimiter + "\\r\\n]*))" - ), "gi"); - - var arrData:any[] = [[]]; - var arrMatches = null; - while (arrMatches = objPattern.exec( strData )){ - var strMatchedDelimiter = arrMatches[ 1 ]; - if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter) { arrData.push( [] ); } - var strMatchedValue; - if (arrMatches[ 2 ]) strMatchedValue = arrMatches[ 2 ].replace(new RegExp( "\"\"", "g" ), "\""); - else strMatchedValue = arrMatches[ 3 ]; - arrData[ arrData.length - 1 ].push( strMatchedValue ); + static CSVToArray(strData: string, strDelimiter: string) { + strDelimiter = strDelimiter || "," + + var objPattern = new RegExp("(\\" + strDelimiter + "|\\r?\\n|\\r|^)" + '(?:"([^"]*(?:""[^"]*)*)"|' + '([^"\\' + strDelimiter + "\\r\\n]*))", "gi") + + var arrData: any[] = [[]] + var arrMatches = null + while ((arrMatches = objPattern.exec(strData))) { + var strMatchedDelimiter = arrMatches[1] + if (strMatchedDelimiter.length && strMatchedDelimiter !== strDelimiter) { + arrData.push([]) + } + var strMatchedValue + if (arrMatches[2]) strMatchedValue = arrMatches[2].replace(new RegExp('""', "g"), '"') + else strMatchedValue = arrMatches[3] + arrData[arrData.length - 1].push(strMatchedValue) } - return( arrData ); + return arrData } - -} \ No newline at end of file +} diff --git a/src/electron/utils/files.ts b/src/electron/utils/files.ts index 506aecc1..9f90f4aa 100644 --- a/src/electron/utils/files.ts +++ b/src/electron/utils/files.ts @@ -14,7 +14,7 @@ import { createThumbnail } from "../data/thumbnails" import { OutputHelper } from "../output/OutputHelper" import { OPEN_FILE } from "./../../types/Channels" import { mainWindow, toApp } from "./../index" -import { getAllShows, trimShow } from "./responses" +import { getAllShows, trimShow } from "./shows" function actionComplete(err: Error | null, actionFailedMessage: string) { if (err) console.error(actionFailedMessage + ":", err) @@ -148,11 +148,11 @@ export function openSystemFolder(path: string) { } const appFolderName = "FreeShow" -export function getDocumentsFolder(p: any = null, folderName: string = "Shows"): string { +export function getDocumentsFolder(p: any = null, folderName: string = "Shows", createFolder: boolean = true): string { let folderPath = [app.getPath("documents"), appFolderName] if (folderName) folderPath.push(folderName) if (!p) p = path.join(...folderPath) - if (!doesPathExist(p)) p = makeDir(p) + if (!doesPathExist(p) && createFolder) p = makeDir(p) return p } @@ -188,11 +188,20 @@ export function getDataFolder(dataPath: string, name: string) { // HELPERS -function createFolder(path: string) { +export function createFolder(path: string) { if (doesPathExist(path)) return path return makeDir(path) } +export function getTimePointString() { + const date = new Date() + let name = date.toISOString() + name = name.slice(0, name.indexOf("T")) + name += `_${("0" + date.getHours()).slice(-2)}-${("0" + date.getMinutes()).slice(-2)}` + + return name +} + export function fileContentMatches(content: string | NodeJS.ArrayBufferView, path: string): boolean { if (doesPathExist(path) && content === readFile(path)) return true return false @@ -657,7 +666,9 @@ const FIXES: any = { }, } function specialCaseFixer() { - let defaultDataFolder = getDocumentsFolder(null, "") + let defaultDataFolder = getDocumentsFolder(null, "", false) + if (!doesPathExist(defaultDataFolder)) return + let files: string[] = readFolder(defaultDataFolder) files.forEach((fileName) => { let matchFound = Object.keys(FIXES).find((key) => fileName.includes(key)) diff --git a/src/electron/utils/responses.ts b/src/electron/utils/responses.ts index 0850fe70..09f7b197 100644 --- a/src/electron/utils/responses.ts +++ b/src/electron/utils/responses.ts @@ -8,7 +8,6 @@ import os from "os" import path from "path" import { closeMain, isProd, mainWindow, maximizeMain, setGlobalMenu, toApp } from ".." import { BIBLE, MAIN, SHOW } from "../../types/Channels" -import { Show } from "../../types/Show" import { restoreFiles } from "../data/backup" import { downloadMedia } from "../data/downloadMedia" import { importShow } from "../data/import" @@ -16,6 +15,7 @@ import { convertPDFToImages } from "../data/pdfToImage" import { config, error_log, stores } from "../data/store" import { getThumbnail, getThumbnailFolderPath, saveImage } from "../data/thumbnails" import { OutputHelper } from "../output/OutputHelper" +import { getPresentationApplications, presentationControl, startSlideshow } from "../output/ppt/presentation" import { closeServers, startServers } from "../servers" import { Message } from "./../../types/Socket" import { startWebSocketAndRest, stopApiListener } from "./api" @@ -23,8 +23,6 @@ import { bundleMediaFiles, checkShowsFolder, dataFolderNames, - deleteFile, - doesPathExist, getDataFolder, getDocumentsFolder, getFileInfo, @@ -35,11 +33,8 @@ import { loadFile, locateMediaFile, openSystemFolder, - parseShow, readExifData, readFile, - readFolder, - renameFile, selectFiles, selectFilesDialog, selectFolder, @@ -47,8 +42,8 @@ import { } from "./files" import { LyricSearch } from "./LyricSearch" import { closeMidiInPorts, getMidiInputs, getMidiOutputs, receiveMidi, sendMidi } from "./midi" +import { deleteShows, deleteShowsNotIndexed, getAllShows, getEmptyShows, refreshAllShows } from "./shows" import checkForUpdates from "./updater" -import { getPresentationApplications, presentationControl, startSlideshow } from "../output/ppt/presentation" // IMPORT export function startImport(_e: any, msg: Message) { @@ -114,8 +109,10 @@ const mainResponses: any = { GET_STORE_VALUE: (data: any) => getStoreValue(data), SET_STORE_VALUE: (data: any) => setStoreValue(data), // SHOWS - DELETE_SHOWS: (data: any) => deleteShowsNotIndexed(data), + DELETE_SHOWS: (data: any) => deleteShows(data), + DELETE_SHOWS_NI: (data: any) => deleteShowsNotIndexed(data), REFRESH_SHOWS: (data: any) => refreshAllShows(data), + GET_EMPTY_SHOWS: (data: any) => getEmptyShows(data), FULL_SHOWS_LIST: (data: any) => getAllShows(data), // OUTPUT GET_SCREENS: (): void => getScreens(), @@ -180,88 +177,6 @@ export function receiveMain(e: any, msg: Message) { ///// HELPERS ///// -// SHOWS -function deleteShowsNotIndexed(data: any) { - // get all names - let names: string[] = Object.entries(data.shows).map(([id, { name }]: any) => (name || id) + ".show") - - // list all shows in folder - let filesInFolder: string[] = readFolder(data.path) - if (!filesInFolder.length) return - - let deleted: string[] = [] - - for (const name of filesInFolder) checkFile(name) - function checkFile(name: string) { - if (names.includes(name) || !name.includes(".show")) return - - let p: string = path.join(data.path, name) - deleteFile(p) - deleted.push(name) - } - - toApp("MAIN", { channel: "DELETE_SHOWS", data: { deleted } }) -} - -export function getAllShows(data: any) { - if (!doesPathExist(data.path)) return [] - - let filesInFolder: string[] = readFolder(data.path).filter((a) => a.includes(".show") && a.length > 5) - return filesInFolder -} - -function refreshAllShows(data: any) { - if (!doesPathExist(data.path)) return - - // list all shows in folder - let filesInFolder: string[] = readFolder(data.path) - if (!filesInFolder.length) return - - let newShows: any = {} - - for (const name of filesInFolder) loadFile(name) - function loadFile(name: string) { - if (!name.includes(".show")) return - - let p: string = path.join(data.path, name) - let show = parseShow(readFile(p)) - - if (!show || !show[1]) return - - newShows[show[0]] = trimShow({ ...show[1], name: name.replace(".show", "") }) - } - - if (!Object.keys(newShows).length) return - toApp("MAIN", { channel: "REFRESH_SHOWS", data: newShows }) -} - -export function renameShows(shows: any, path: string) { - for (const show of shows) checkFile(show) - function checkFile(show: any) { - let oldName = show.oldName + ".show" - let newName = (show.name || show.id) + ".show" - - renameFile(path, oldName, newName) - } -} - -// WIP duplicate of setShow.ts -export function trimShow(showCache: Show) { - let show: any = {} - if (!showCache) return show - - show = { - name: showCache.name, - category: showCache.category, - timestamps: showCache.timestamps, - quickAccess: showCache.quickAccess || {}, - } - if (showCache.private) show.private = true - if (showCache.locked) show.locked = true - - return show -} - // URL: open url in default web browser export const openURL = (url: string) => { shell.openExternal(url) diff --git a/src/electron/utils/shows.ts b/src/electron/utils/shows.ts new file mode 100644 index 00000000..e8dde0a4 --- /dev/null +++ b/src/electron/utils/shows.ts @@ -0,0 +1,152 @@ +import path from "path" +import { toApp } from ".." +import { Show } from "../../types/Show" +import { deleteFile, doesPathExist, parseShow, readFile, readFolder, renameFile } from "./files" + +export function getAllShows(data: any) { + if (!doesPathExist(data.path)) return [] + + let filesInFolder: string[] = readFolder(data.path).filter((a) => a.includes(".show") && a.length > 5) + return filesInFolder +} + +export function renameShows(shows: any, path: string) { + for (const show of shows) checkFile(show) + function checkFile(show: any) { + let oldName = show.oldName + ".show" + let newName = (show.name || show.id) + ".show" + + renameFile(path, oldName, newName) + } +} + +// WIP duplicate of setShow.ts +export function trimShow(showCache: Show) { + let show: any = {} + if (!showCache) return show + + show = { + name: showCache.name, + category: showCache.category, + timestamps: showCache.timestamps, + quickAccess: showCache.quickAccess || {}, + } + if (showCache.private) show.private = true + if (showCache.locked) show.locked = true + + return show +} + +///// + +// let hasContent = !!Object.values(show.slides).find((slide) => slide.items.find((item) => item.lines?.find((line) => line.text?.find((text) => text.value?.length)))) + +function showHasLayoutContent(show: Show) { + return !!Object.values(show.layouts).find((layout) => layout.slides.length) +} + +export function getShowTextContent(show: Show) { + let textContent = "" + Object.values(show.slides).forEach((slide) => { + slide.items.forEach((item) => { + item.lines?.forEach((line) => { + line.text?.forEach((text) => { + textContent += text.value + }) + }) + }) + }) + return textContent +} + +///// + +export function deleteShows(data: any) { + let deleted: string[] = [] + + data.shows.forEach(({ id, name }: any) => { + name = (name || id) + ".show" + let p: string = path.join(data.path, name) + + deleteFile(p) + deleted.push(name) + }) + + refreshAllShows(data) + toApp("MAIN", { channel: "DELETE_SHOWS", data: { deleted } }) +} + +export function deleteShowsNotIndexed(data: any) { + // get all names + let names: string[] = Object.entries(data.shows).map(([id, { name }]: any) => (name || id) + ".show") + + // list all shows in folder + let filesInFolder: string[] = readFolder(data.path) + if (!filesInFolder.length) return + + let deleted: string[] = [] + + for (const name of filesInFolder) checkFile(name) + function checkFile(name: string) { + if (names.includes(name) || !name.includes(".show")) return + + let p: string = path.join(data.path, name) + deleteFile(p) + deleted.push(name) + } + + toApp("MAIN", { channel: "DELETE_SHOWS", data: { deleted } }) +} + +export function refreshAllShows(data: any) { + if (!doesPathExist(data.path)) return + + // list all shows in folder + let filesInFolder: string[] = readFolder(data.path) + if (!filesInFolder.length) return + + let newShows: any = {} + + for (const name of filesInFolder) loadFile(name) + function loadFile(name: string) { + if (!name.includes(".show")) return + + let p: string = path.join(data.path, name) + let show = parseShow(readFile(p)) + + if (!show || !show[1]) return + + newShows[show[0]] = trimShow({ ...show[1], name: name.replace(".show", "") }) + } + + if (!Object.keys(newShows).length) return + toApp("MAIN", { channel: "REFRESH_SHOWS", data: newShows }) +} + +export function getEmptyShows(data: any) { + if (!doesPathExist(data.path)) return [] + + // list all shows in folder + let filesInFolder: string[] = readFolder(data.path) + if (!filesInFolder.length) return [] + + let emptyShows: { id: string; name: string }[] = [] + + for (const name of filesInFolder) loadFile(name) + function loadFile(name: string) { + if (!name.includes(".show")) return + + let p: string = path.join(data.path, name) + let show = parseShow(readFile(p)) + if (!show || !show[1]) return + + // replace stored data with new unsaved cached data + if (data.cached?.[show[0]]) show[1] = data.cached[show[0]] + // check that it is empty + if (showHasLayoutContent(show[1]) || getShowTextContent(show[1]).length) return + + emptyShows.push({ id: show[0], name: name.replace(".show", "") }) + } + + return emptyShows +} diff --git a/src/frontend/classes/Show.ts b/src/frontend/classes/Show.ts index 08fc806f..84a691e7 100644 --- a/src/frontend/classes/Show.ts +++ b/src/frontend/classes/Show.ts @@ -12,6 +12,17 @@ export class ShowObj implements Show { settings: any timestamps: any quickAccess: any + message?: { + text: string + template?: string + } + metadata?: { + autoMedia?: boolean + override: boolean + display: string + template: string + tags?: string[] + } meta: any slides: any layouts: any diff --git a/src/frontend/components/actions/customActivation.ts b/src/frontend/components/actions/customActivation.ts index 3d14581f..066a426b 100644 --- a/src/frontend/components/actions/customActivation.ts +++ b/src/frontend/components/actions/customActivation.ts @@ -12,6 +12,8 @@ export const customActionActivations = [ { id: "slide_click", name: "$:actions.activate_slide_clicked:$" }, { id: "video_start", name: "$:actions.activate_video_starting:$" }, { id: "video_end", name: "$:actions.activate_video_ending:$" }, + { id: "audio_start", name: "$:actions.activate_audio_starting:$" }, + { id: "audio_end", name: "$:actions.activate_audio_ending:$" }, { id: "timer_end", name: "$:actions.activate_timer_ending:$" }, { id: "scripture_start", name: "$:actions.activate_scripture_start:$" }, { id: "slide_cleared", name: "$:actions.activate_slide_cleared:$" }, diff --git a/src/frontend/components/drawer/Drawer.svelte b/src/frontend/components/drawer/Drawer.svelte index b117d5b8..af85cdb4 100644 --- a/src/frontend/components/drawer/Drawer.svelte +++ b/src/frontend/components/drawer/Drawer.svelte @@ -15,7 +15,7 @@ const minHeight = 40 const topHeight = 40 - let maxHeight = window.innerHeight - topHeight - ($os.platform === "win32" ? MENU_BAR_HEIGHT : 0) + let maxHeight = window.innerHeight - topHeight - ($os.platform === "win32" ? MENU_BAR_HEIGHT - 0.3 : 0) $: height = $drawer.height let move: boolean = false @@ -23,7 +23,7 @@ function mousedown(e: any) { if (e.target.closest(".search")) return - maxHeight = window.innerHeight - topHeight - ($os.platform === "win32" ? MENU_BAR_HEIGHT : 0) + maxHeight = window.innerHeight - topHeight - ($os.platform === "win32" ? MENU_BAR_HEIGHT - 0.3 : 0) mouse = { x: e.clientX, y: e.clientY, diff --git a/src/frontend/components/drawer/Navigation.svelte b/src/frontend/components/drawer/Navigation.svelte index 79378d02..7cf3e86a 100644 --- a/src/frontend/components/drawer/Navigation.svelte +++ b/src/frontend/components/drawer/Navigation.svelte @@ -67,7 +67,7 @@ buttons = getBibleVersions() } else if (id === "calendar") { buttons = [ - { id: "event", name: "calendar.event", default: true, icon: "calendar" }, + { id: "event", name: "menu._title_calendar", default: true, icon: "calendar" }, { id: "action", name: "calendar.schedule_action", default: true, icon: "actions" }, // WIP very few tabs ] diff --git a/src/frontend/components/drawer/pages/Templates.svelte b/src/frontend/components/drawer/pages/Templates.svelte index 35eb3a59..ae2b4497 100644 --- a/src/frontend/components/drawer/pages/Templates.svelte +++ b/src/frontend/components/drawer/pages/Templates.svelte @@ -70,7 +70,7 @@ if (!$activeShow || ($activeShow?.type || "show") !== "show" || e.ctrlKey || e.metaKey) return if ($showsCache[$activeShow.id]?.locked) return - history({ id: "TEMPLATE", newData: { id: template.id, data: { createItems: true } }, location: { page: "none", override: "show#" + $activeShow.id } }) + history({ id: "TEMPLATE", newData: { id: template.id, data: { createItems: true, shiftItems: e.shiftKey } }, location: { page: "none", override: "show#" + $activeShow.id } }) }} > diff --git a/src/frontend/components/helpers/array.ts b/src/frontend/components/helpers/array.ts index fd977966..c222a288 100644 --- a/src/frontend/components/helpers/array.ts +++ b/src/frontend/components/helpers/array.ts @@ -133,6 +133,24 @@ export function removeDeleted(object: T): T { return (object as any).filter((o) => !o.deleted) } +// remove every duplicated values in object +export function removeDuplicateValues(obj: T): T { + if (typeof obj !== "object") return obj + + let uniqueObj: T = {} as T + const valueSet = new Set() + + for (const [key, value] of Object.entries(obj!)) { + const valueStr = JSON.stringify(value) + if (!valueSet.has(valueStr)) { + valueSet.add(valueStr) + uniqueObj[key] = value + } + } + + return uniqueObj +} + // change values from one object to another export function changeValues(object: any, values: { [key: string]: any }) { Object.entries(values).forEach(([key, value]: any) => { diff --git a/src/frontend/components/helpers/audio.ts b/src/frontend/components/helpers/audio.ts index fdb5b18a..046a9d14 100644 --- a/src/frontend/components/helpers/audio.ts +++ b/src/frontend/components/helpers/audio.ts @@ -38,6 +38,26 @@ export async function playAudio({ path, name = "", audio = null, stream = null } let encodedPath = encodeFilePath(path) audio = audio || new Audio(encodedPath) + + // LISTENERS + + audio.addEventListener("error", (err) => { + console.error("Could not get audio:", err) + + playingAudio.update((a) => { + delete a[path] + return a + }) + }) + + let readyToPlay: boolean = false + audio.addEventListener("canplay", () => { + readyToPlay = true + if (get(playingAudio)[path]?.audio) initAudio() + }) + + ///// + let analyser: any = await getAnalyser(audio, stream) // another audio might have been started while awaiting (if played rapidly) @@ -70,24 +90,17 @@ export async function playAudio({ path, name = "", audio = null, stream = null } if (startAt > 0) audio.currentTime = startAt - audio.addEventListener("error", (err) => { - console.error("Could not get audio:", err) - - playingAudio.update((a) => { - delete a[path] - return a - }) - }) - - audio.addEventListener("canplay", () => { + if (readyToPlay) initAudio() + function initAudio() { setTimeout(() => { // audio might have been cleared if (!get(playingAudio)[path]?.audio) return get(playingAudio)[path].audio.play() + customActionActivation("audio_start") analyseAudio() }, waitToPlay * 1000) - }) + } } let currentlyCrossfading: string[] = [] @@ -576,6 +589,7 @@ export function clearAudio(path: string = "", clearPlaylist: boolean = true) { if (!a[path]?.audio) return deleteAudio(path) a[path].audio.pause() + customActionActivation("audio_end") deleteAudio(path) } diff --git a/src/frontend/components/helpers/historyActions.ts b/src/frontend/components/helpers/historyActions.ts index d23206f1..15167ab2 100644 --- a/src/frontend/components/helpers/historyActions.ts +++ b/src/frontend/components/helpers/historyActions.ts @@ -728,6 +728,7 @@ export const historyActions = ({ obj, undo = null }: any) => { let slideId: string = data.indexes ? ref[data.indexes[0]]?.id : "" let createItems: boolean = !!data.data?.createItems + let shiftItems: boolean = !!data.data?.shiftItems if (deleting) { let previousData = data.previousData @@ -788,7 +789,7 @@ export const historyActions = ({ obj, undo = null }: any) => { // roll items around let newTemplate = data.previousData.template !== data.id - if (createItems && !slide.settings?.template && !newTemplate) slide.items = [...slide.items.slice(1), slide.items[0]].filter((a) => a) + if (shiftItems && !slide.settings?.template && !newTemplate) slide.items = [...slide.items.slice(1), slide.items[0]].filter((a) => a) let changeOverflowItems = slide.settings?.template || createItems let newItems = mergeWithTemplate(slide.items, slideTemplate.items, changeOverflowItems, obj.save !== false) diff --git a/src/frontend/components/helpers/output.ts b/src/frontend/components/helpers/output.ts index 0e548cd3..2f70e740 100644 --- a/src/frontend/components/helpers/output.ts +++ b/src/frontend/components/helpers/output.ts @@ -24,6 +24,7 @@ import { theme, themes, transitionData, + usageLog, videoExtensions, } from "../../stores" import { send } from "../../utils/request" @@ -63,6 +64,9 @@ export function setOutput(key: string, data: any, toggle: boolean = false, outpu let firstOutputWithBackground = allOutputs.findIndex((id) => (get(styles)[get(outputs)[id]?.style || ""]?.layers || ["background"]).includes("background")) firstOutputWithBackground = Math.max(0, firstOutputWithBackground) + // append show usage if not already outputted + if (key === "slide" && data?.id && get(outputs)[outs[0]]?.out?.slide?.id !== data?.id) appendShowUsage(data.id) + outs.forEach((id: string) => { let output: any = a[id] if (!output.out) a[id].out = {} @@ -100,6 +104,26 @@ export function setOutput(key: string, data: any, toggle: boolean = false, outpu }) } +function appendShowUsage(showId: string) { + let show = get(showsCache)[showId] + if (!show) return + + usageLog.update((a) => { + let metadata = show.meta || {} + // remove empty values + Object.keys(metadata).forEach((key) => { + if (!metadata[key]) delete metadata[key] + }) + + a.all.push({ + name: show.name, + time: Date.now(), + metadata, + }) + return a + }) +} + function changeOutputBackground(data, { output, id, mute }) { if (get(currentWindow) === null) { setTimeout(() => { @@ -258,8 +282,9 @@ export function outputSlideHasContent(output) { // WIP style should override any slide resolution & color ? (it does not) -export function getResolution(initial: Resolution | undefined | null = null, _updater: any = null, getSlideRes: boolean = false): Resolution { - let currentOutput = get(outputs)[getActiveOutputs()[0]] +export function getResolution(initial: Resolution | undefined | null = null, _updater: any = null, getSlideRes: boolean = false, outputId: string = ""): Resolution { + if (!outputId) outputId = getActiveOutputs()[0] + let currentOutput = get(outputs)[outputId] let style = currentOutput?.style ? get(styles)[currentOutput?.style]?.resolution : null let slideRes: any = null @@ -729,6 +754,10 @@ export function getStyleTemplate(outSlide: any, currentStyle: any) { return template } +export function slideHasAutoSizeItem(slide: any) { + return slide?.items?.find((a) => a.auto) +} + export function setTemplateStyle(outSlide: any, currentStyle: any, items: Item[]) { let isDrawerScripture = outSlide?.id === "temp" let slideItems = isDrawerScripture ? outSlide.tempItems : items diff --git a/src/frontend/components/helpers/time.ts b/src/frontend/components/helpers/time.ts index 8e500ca1..2ec279f5 100644 --- a/src/frontend/components/helpers/time.ts +++ b/src/frontend/components/helpers/time.ts @@ -151,3 +151,10 @@ export function timeAgo(time: number) { const count = Math.floor(seconds / interval.seconds) return `${count} ${interval.label}${count !== 1 ? "s" : ""} ago` } + +export function getTimeFromInterval(interval) { + if (interval === "daily") return 86400000 + if (interval === "weekly") return 604800000 + if (interval === "mothly") return 2592000000 + return 0 +} diff --git a/src/frontend/components/inputs/Button.svelte b/src/frontend/components/inputs/Button.svelte index 448786a9..d0008768 100644 --- a/src/frontend/components/inputs/Button.svelte +++ b/src/frontend/components/inputs/Button.svelte @@ -184,6 +184,7 @@ /* padding: 0 0.5em; */ /* padding-left: 0.2em; */ box-sizing: content-box; + border: 0 !important; /* remove CombinedInput border */ } button.active :global(svg) { fill: var(--text); diff --git a/src/frontend/components/inputs/MediaPicker.svelte b/src/frontend/components/inputs/MediaPicker.svelte index 809b1db7..bb1704e4 100644 --- a/src/frontend/components/inputs/MediaPicker.svelte +++ b/src/frontend/components/inputs/MediaPicker.svelte @@ -10,6 +10,7 @@ export let title: string = "" export let multiple: boolean = false export let clearOnClick: boolean = false + export let center: boolean = true function pick() { if (clearOnClick) { @@ -33,6 +34,6 @@ } - diff --git a/src/frontend/components/inputs/TextArea.svelte b/src/frontend/components/inputs/TextArea.svelte index 5940e21c..4d5be3bd 100644 --- a/src/frontend/components/inputs/TextArea.svelte +++ b/src/frontend/components/inputs/TextArea.svelte @@ -4,10 +4,11 @@ export let style: string = "" export let center: boolean = false + export let disabled: boolean = false export let autofocus: boolean = false -