From 546810a6a6a7cd5dc8ccb59bed4bd4d442fa0796 Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:27:47 +0200 Subject: [PATCH 01/25] add hideInNav flag to plugins (optional) --- public/translations/de.json | 44 +++++++------- public/translations/en.json | 2 + public/translations/es.json | 44 +++++++------- public/translations/fr.json | 44 +++++++------- public/translations/it.json | 44 +++++++------- public/translations/pl.json | 44 +++++++------- .../buttons/PluginTemplateDownloadButton.tsx | 4 +- src/components/community/CommunitySubNav.tsx | 36 +++++------ .../community/plugincard/PluginCard.tsx | 59 +++++++++++++++---- .../community/pluginlist/PluginList.tsx | 26 ++++---- .../pluginlist/modals/PluginHelpModal.tsx | 4 +- src/components/home/HomeSubNav.tsx | 2 +- src/components/settings/SettingsSubNav.tsx | 2 +- .../tonieboxes/TonieboxesSubNav.tsx | 2 +- src/components/tonies/ToniesSubNav.tsx | 2 +- src/provider/TeddyCloudProvider.tsx | 1 + src/types/pluginsMetaTypes.ts | 1 + 17 files changed, 206 insertions(+), 155 deletions(-) diff --git a/public/translations/de.json b/public/translations/de.json index bb77ab82..5e5574dd 100644 --- a/public/translations/de.json +++ b/public/translations/de.json @@ -21,11 +21,11 @@ "password": "Passwort", "passwordMin": "Mindestens 4 Zeichen", "passwordRequired": "Passwort ist erforderlich", - "username": "Benutzername", - "usernameRequired": "Benutzername ist erforderlich", + "retry": "Erneut versuchen", "statusLoadFailed": "Authentifizierungsstatus konnte nicht geladen werden", "statusLoadFailedDetails": "Der Authentifizierungsstatus konnte nicht geladen werden. Prüfe die Verbindung zu TeddyCloud und versuche es erneut.", - "retry": "Erneut versuchen" + "username": "Benutzername", + "usernameRequired": "Benutzername ist erforderlich" }, "common": { "cancel": "Abbrechen" @@ -142,6 +142,7 @@ "fields": { "author": "Autor des Plugins", "description": "Kurze Zusammenfassung des Plugins", + "hideInNav": "Flag, das angibt, ob das Plugin in der Navigation ausgeblendet werden soll (true/false)", "icon": "Das für das Plugin verwendete Icon. Muss eine gültige Ant Design Icon-Komponente von https://ant.design/components/icon sein, z. B. TrophyOutlined, TagsOutlined", "pluginHomepage": "Homepage des Plugins", "pluginName": "Der Titel, der in Menüs angezeigt wird (erforderlich)", @@ -157,6 +158,7 @@ }, "helpButton": "Hilfe", "helpTextCreatePlugin": "Ein Text darüber, wie man ein Plugin erstellt", + "hidden": "Versteckt", "installedPlugins": "Installierte Plugins", "intro": "Hier findest du deine hinzugefügten TeddyCloud Plugins und kannst neue Plugins von dir oder der TeddyCloud Community hinzufügen!", "navigationTitle": "TC Plugins", @@ -390,10 +392,18 @@ "filter": "Filter im aktuellen Ordner", "help": { "actionItems": { + "assignToTonie": { + "text": "Weist die ausgewählte Datei einem Tonie/Tag als Inhalt zu.", + "title": "Tonie zuweisen" + }, "deleteFileFolder": { "text": "Löscht die Datei oder den Ordner (nur wenn der Ordner leer ist). Hinweis: Wenn diese Datei einem Tonie/Tag zugewiesen ist, wird der Verweis nicht aktualisiert.", "title": "Datei/Ordner löschen" }, + "downloadFile": { + "text": "Lädt die ausgewählte Datei herunter oder lädt TAF-Dateien als OGG-Datei herunter.", + "title": "Datei herunterladen" + }, "duplicateTAPFile": { "text": "Erstellt eine Kopie der ausgewählten TAP-Datei.", "title": "TAP-Datei duplizieren" @@ -406,6 +416,16 @@ "text": "Öffnet den Tonie-Audio-Playlist-Editor, um TAP-Dateien zu verwalten.", "title": "TAP-Playlist bearbeiten" }, + "markAsListened": { + "listened": { + "text": "Die Datei ist derzeit als gehört markiert. Klicke, um sie als nicht gehört zu markieren.", + "title": "Als nicht gehört markieren" + }, + "unlistened": { + "text": "Die Datei ist derzeit als nicht gehört markiert. Klicke, um sie als gehört zu markieren.", + "title": "Als gehört markieren" + } + }, "migrateToAudioIdFolder": { "text": "Verschiebt die TAF-Datei in den Ordner 'by/audioId' der Library und aktualisiert die Quelle in der JSON-Datei.", "title": "TAF-Datei nach AudioId in die Library verschieben" @@ -425,24 +445,6 @@ "renameFile": { "text": "Benennt die Datei um. Hinweis: Wenn diese Datei einem Tonie/Tag zugewiesen ist, wird der Verweis nicht automatisch aktualisiert.", "title": "Datei umbenennen" - }, - "downloadFile": { - "title": "Datei herunterladen", - "text": "Lädt die ausgewählte Datei herunter oder lädt TAF-Dateien als OGG-Datei herunter." - }, - "markAsListened": { - "unlistened": { - "title": "Als gehört markieren", - "text": "Die Datei ist derzeit als nicht gehört markiert. Klicke, um sie als gehört zu markieren." - }, - "listened": { - "title": "Als nicht gehört markieren", - "text": "Die Datei ist derzeit als gehört markiert. Klicke, um sie als nicht gehört zu markieren." - } - }, - "assignToTonie": { - "title": "Tonie zuweisen", - "text": "Weist die ausgewählte Datei einem Tonie/Tag als Inhalt zu." } }, "actionItemsMulti": { diff --git a/public/translations/en.json b/public/translations/en.json index 7f60e20d..41241e66 100644 --- a/public/translations/en.json +++ b/public/translations/en.json @@ -142,6 +142,7 @@ "fields": { "author": "Author of the plugin", "description": "Short summary of the plugin", + "hideInNav": "Flag indicating whether the plugin should be hidden in navigation (true/false)", "icon": "The icon used for the plugin. Must be a valid Ant Design icon component from https://ant.design/components/icon, e.g. TrophyOutlined, TagsOutlined", "pluginHomepage": "Homepage of the plugin", "pluginName": "The title shown in menus (mandatory)", @@ -157,6 +158,7 @@ }, "helpButton": "Help", "helpTextCreatePlugin": "Some Text about how to create a plugin", + "hidden": "Hidden", "installedPlugins": "Installed plugins", "intro": "Here you find your added TeddyCloud plugins and you can add new plugins provided by you or the TeddyCloud community!", "navigationTitle": "TC Plugins", diff --git a/public/translations/es.json b/public/translations/es.json index 62330ec8..abd10683 100644 --- a/public/translations/es.json +++ b/public/translations/es.json @@ -21,11 +21,11 @@ "password": "Contraseña", "passwordMin": "Al menos 4 caracteres", "passwordRequired": "La contraseña es obligatoria", - "username": "Usuario", - "usernameRequired": "El usuario es obligatorio", + "retry": "Reintentar", "statusLoadFailed": "No se pudo cargar el estado de autenticación", "statusLoadFailedDetails": "No se pudo cargar el estado de autenticación. Comprueba la conexión con TeddyCloud e inténtalo de nuevo.", - "retry": "Reintentar" + "username": "Usuario", + "usernameRequired": "El usuario es obligatorio" }, "common": { "cancel": "Cancelar" @@ -142,6 +142,7 @@ "fields": { "author": "Autor del plugin", "description": "Resumen breve del plugin", + "hideInNav": "Indicador que define si el plugin debe ocultarse en la navegación (true/false)", "icon": "El icono utilizado para el plugin. Debe ser un componente de icono válido de Ant Design de https://ant.design/components/icon, p. ej. TrophyOutlined, TagsOutlined", "pluginHomepage": "Página principal del plugin", "pluginName": "El título que aparece en los menús (obligatorio)", @@ -157,6 +158,7 @@ }, "helpButton": "Ayuda", "helpTextCreatePlugin": "Texto sobre cómo crear un plugin", + "hidden": "Oculto", "installedPlugins": "Plugins instalados", "intro": "Aquí puedes ver tus plugins añadidos a TeddyCloud y también agregar nuevos plugins creados por ti o por la comunidad TeddyCloud.", "navigationTitle": "Plugins TC", @@ -390,10 +392,18 @@ "filter": "Filtrar dentro de la carpeta actual", "help": { "actionItems": { + "assignToTonie": { + "text": "Asigna el archivo seleccionado como contenido a un Tonie/Tag.", + "title": "Asignar a Tonie" + }, "deleteFileFolder": { "text": "Elimina el archivo o carpeta (solo si la carpeta está vacía). Nota: Si este archivo está asignado a un Tonie/Tag, la referencia no se actualizará.", "title": "Eliminar archivo/carpeta" }, + "downloadFile": { + "text": "Descarga el archivo seleccionado o, en el caso de archivos TAF, lo descarga como archivo OGG.", + "title": "Descargar archivo" + }, "duplicateTAPFile": { "text": "Crea una copia del archivo TAP seleccionado.", "title": "Duplicar archivo TAP" @@ -406,6 +416,16 @@ "text": "Abre el editor de TeddyAudio Playlist de Tonie para administrar archivos TAP.", "title": "Editar lista de reproducción TAP" }, + "markAsListened": { + "listened": { + "text": "El archivo está marcado actualmente como escuchado. Haz clic para marcarlo como no escuchado.", + "title": "Marcar como no escuchado" + }, + "unlistened": { + "text": "El archivo está marcado actualmente como no escuchado. Haz clic para marcarlo como escuchado.", + "title": "Marcar como escuchado" + } + }, "migrateToAudioIdFolder": { "text": "Mueve el archivo TAF a la carpeta 'by/audioId' de la biblioteca y actualiza la fuente en el archivo JSON.", "title": "Migrar archivo TAF a la biblioteca por AudioId" @@ -425,24 +445,6 @@ "renameFile": { "text": "Renombra el archivo. Nota: Si este archivo está asignado a un Tonie/Tag, la referencia no se actualizará automáticamente.", "title": "Renombrar archivo" - }, - "downloadFile": { - "title": "Descargar archivo", - "text": "Descarga el archivo seleccionado o, en el caso de archivos TAF, lo descarga como archivo OGG." - }, - "markAsListened": { - "unlistened": { - "title": "Marcar como escuchado", - "text": "El archivo está marcado actualmente como no escuchado. Haz clic para marcarlo como escuchado." - }, - "listened": { - "title": "Marcar como no escuchado", - "text": "El archivo está marcado actualmente como escuchado. Haz clic para marcarlo como no escuchado." - } - }, - "assignToTonie": { - "title": "Asignar a Tonie", - "text": "Asigna el archivo seleccionado como contenido a un Tonie/Tag." } }, "actionItemsMulti": { diff --git a/public/translations/fr.json b/public/translations/fr.json index 95c5ae65..1a1f646e 100644 --- a/public/translations/fr.json +++ b/public/translations/fr.json @@ -21,11 +21,11 @@ "password": "Mot de passe", "passwordMin": "Au moins 4 caractères", "passwordRequired": "Le mot de passe est requis", - "username": "Nom d'utilisateur", - "usernameRequired": "Le nom d'utilisateur est requis", + "retry": "Réessayer", "statusLoadFailed": "Impossible de charger l’état d’authentification", "statusLoadFailedDetails": "L’état d’authentification n’a pas pu être chargé. Vérifiez la connexion à TeddyCloud et réessayez.", - "retry": "Réessayer" + "username": "Nom d'utilisateur", + "usernameRequired": "Le nom d'utilisateur est requis" }, "common": { "cancel": "Annuler" @@ -142,6 +142,7 @@ "fields": { "author": "Auteur du plugin", "description": "Résumé court du plugin", + "hideInNav": "Indicateur précisant si le plugin doit être masqué dans la navigation (true/false)", "icon": "L'icône utilisée pour le plugin. Doit être un composant d'icône Ant Design valide depuis https://ant.design/components/icon, par ex. TrophyOutlined, TagsOutlined", "pluginHomepage": "Page d'accueil du plugin", "pluginName": "Le titre affiché dans les menus (obligatoire)", @@ -157,6 +158,7 @@ }, "helpButton": "Aide", "helpTextCreatePlugin": "Un texte sur la création d'un plugin", + "hidden": "Masqué", "installedPlugins": "Plugins installés", "intro": "Vous trouverez ici vos plugins TeddyCloud ajoutés et pouvez ajouter de nouveaux plugins fournis par vous-même ou la communauté TeddyCloud !", "navigationTitle": "Plugins TC", @@ -390,10 +392,18 @@ "filter": "Filtrer dans le dossier actuel", "help": { "actionItems": { + "assignToTonie": { + "text": "Assigne le fichier sélectionné comme contenu à un Tonie/Tag.", + "title": "Assigner à un Tonie" + }, "deleteFileFolder": { "text": "Supprime le fichier ou le dossier (seulement si le dossier est vide). Remarque : si ce fichier est attribué à un Tonie/Tag, la référence ne sera pas mise à jour.", "title": "Supprimer le fichier/dossier" }, + "downloadFile": { + "text": "Télécharge le fichier sélectionné ou, pour les fichiers TAF, le télécharge au format OGG.", + "title": "Télécharger le fichier" + }, "duplicateTAPFile": { "text": "Crée une copie du fichier TAP sélectionné.", "title": "Dupliquer le fichier TAP" @@ -406,6 +416,16 @@ "text": "Ouvre l'éditeur de TeddyAudio Playlist pour gérer les fichiers TAP.", "title": "Modifier la liste de lecture TAP" }, + "markAsListened": { + "listened": { + "text": "Le fichier est actuellement marqué comme écouté. Cliquez pour le marquer comme non écouté.", + "title": "Marquer comme non écouté" + }, + "unlistened": { + "text": "Le fichier est actuellement marqué comme non écouté. Cliquez pour le marquer comme écouté.", + "title": "Marquer comme écouté" + } + }, "migrateToAudioIdFolder": { "text": "Déplace le fichier TAF dans le dossier 'by/audioId' de la bibliothèque et met à jour la source dans le fichier JSON.", "title": "Migrer le fichier TAF dans la bibliothèque par AudioId" @@ -425,24 +445,6 @@ "renameFile": { "text": "Renomme le fichier. Remarque : si ce fichier est attribué à un Tonie/Tag, la référence ne sera pas mise à jour automatiquement.", "title": "Renommer le fichier" - }, - "downloadFile": { - "title": "Télécharger le fichier", - "text": "Télécharge le fichier sélectionné ou, pour les fichiers TAF, le télécharge au format OGG." - }, - "markAsListened": { - "unlistened": { - "title": "Marquer comme écouté", - "text": "Le fichier est actuellement marqué comme non écouté. Cliquez pour le marquer comme écouté." - }, - "listened": { - "title": "Marquer comme non écouté", - "text": "Le fichier est actuellement marqué comme écouté. Cliquez pour le marquer comme non écouté." - } - }, - "assignToTonie": { - "title": "Assigner à un Tonie", - "text": "Assigne le fichier sélectionné comme contenu à un Tonie/Tag." } }, "actionItemsMulti": { diff --git a/public/translations/it.json b/public/translations/it.json index 79b97ed7..e21c595f 100644 --- a/public/translations/it.json +++ b/public/translations/it.json @@ -21,11 +21,11 @@ "password": "Password", "passwordMin": "Almeno 4 caratteri", "passwordRequired": "La password è obbligatoria", - "username": "Nome utente", - "usernameRequired": "Il nome utente è obbligatorio", + "retry": "Riprova", "statusLoadFailed": "Impossibile caricare lo stato di autenticazione", "statusLoadFailedDetails": "Impossibile caricare lo stato di autenticazione. Controlla la connessione a TeddyCloud e riprova.", - "retry": "Riprova" + "username": "Nome utente", + "usernameRequired": "Il nome utente è obbligatorio" }, "common": { "cancel": "Annulla" @@ -142,6 +142,7 @@ "fields": { "author": "Autore del plugin", "description": "Breve descrizione del plugin", + "hideInNav": "Flag che indica se il plugin deve essere nascosto nella navigazione (true/false)", "icon": "L'icona utilizzata per il plugin. Deve essere un componente icona Ant Design valido da https://ant.design/components/icon, ad es. TrophyOutlined, TagsOutlined", "pluginHomepage": "Homepage del plugin", "pluginName": "Il titolo mostrato nei menu (obbligatorio)", @@ -157,6 +158,7 @@ }, "helpButton": "Aiuto", "helpTextCreatePlugin": "Testo su come creare un plugin", + "hidden": "Nascosto", "installedPlugins": "Plugin installati", "intro": "Qui trovi i plugin TeddyCloud che hai aggiunto e puoi aggiungere nuovi plugin forniti da te o dalla comunità TeddyCloud!", "navigationTitle": "Plugin TC", @@ -390,10 +392,18 @@ "filter": "Filtra nella cartella corrente", "help": { "actionItems": { + "assignToTonie": { + "text": "Assegna il file selezionato come contenuto a un Tonie/Tag.", + "title": "Assegna a Tonie" + }, "deleteFileFolder": { "text": "Elimina il file o la cartella (solo se la cartella è vuota). Nota: se questo file è assegnato a un Tonie/Tag, il riferimento non verrà aggiornato.", "title": "Elimina file/cartella" }, + "downloadFile": { + "text": "Scarica il file selezionato oppure, per i file TAF, lo scarica come file OGG.", + "title": "Scarica file" + }, "duplicateTAPFile": { "text": "Crea un duplicato del file TAP selezionato.", "title": "Duplica file TAP" @@ -406,6 +416,16 @@ "text": "Apre l'editor di playlist TeddyAudio per gestire i file TAP.", "title": "Modifica playlist TAP" }, + "markAsListened": { + "listened": { + "text": "Il file è attualmente segnato come ascoltato. Fai clic per segnarlo come non ascoltato.", + "title": "Segna come non ascoltato" + }, + "unlistened": { + "text": "Il file è attualmente segnato come non ascoltato. Fai clic per segnarlo come ascoltato.", + "title": "Segna come ascoltato" + } + }, "migrateToAudioIdFolder": { "text": "Sposta il file TAF nella cartella 'by/audioId' della libreria e aggiorna la sorgente nel file JSON.", "title": "Migra file TAF nella libreria per AudioId" @@ -425,24 +445,6 @@ "renameFile": { "text": "Rinomina il file. Nota: se questo file è assegnato a un Tonie/Tag, il riferimento non verrà aggiornato automaticamente.", "title": "Rinomina file" - }, - "downloadFile": { - "title": "Scarica file", - "text": "Scarica il file selezionato oppure, per i file TAF, lo scarica come file OGG." - }, - "markAsListened": { - "unlistened": { - "title": "Segna come ascoltato", - "text": "Il file è attualmente segnato come non ascoltato. Fai clic per segnarlo come ascoltato." - }, - "listened": { - "title": "Segna come non ascoltato", - "text": "Il file è attualmente segnato come ascoltato. Fai clic per segnarlo come non ascoltato." - } - }, - "assignToTonie": { - "title": "Assegna a Tonie", - "text": "Assegna il file selezionato come contenuto a un Tonie/Tag." } }, "actionItemsMulti": { diff --git a/public/translations/pl.json b/public/translations/pl.json index f77a6d65..c80f5a34 100644 --- a/public/translations/pl.json +++ b/public/translations/pl.json @@ -21,11 +21,11 @@ "password": "Hasło", "passwordMin": "Co najmniej 4 znaki", "passwordRequired": "Hasło jest wymagane", - "username": "Nazwa użytkownika", - "usernameRequired": "Nazwa użytkownika jest wymagana", + "retry": "Spróbuj ponownie", "statusLoadFailed": "Nie udało się wczytać stanu uwierzytelniania", "statusLoadFailedDetails": "Nie udało się wczytać stanu uwierzytelniania. Sprawdź połączenie z TeddyCloud i spróbuj ponownie.", - "retry": "Spróbuj ponownie" + "username": "Nazwa użytkownika", + "usernameRequired": "Nazwa użytkownika jest wymagana" }, "common": { "cancel": "Anuluj" @@ -142,6 +142,7 @@ "fields": { "author": "Autor wtyczki", "description": "Krótki opis wtyczki", + "hideInNav": "Flaga określająca, czy wtyczka ma być ukryta w nawigacji (true/false)", "icon": "Ikona używana dla wtyczki. Musi być prawidłowym komponentem ikony Ant Design z https://ant.design/components/icon, np. TrophyOutlined, TagsOutlined", "pluginHomepage": "Strona domowa wtyczki", "pluginName": "Tytuł wyświetlany w menu (wymagane)", @@ -157,6 +158,7 @@ }, "helpButton": "Pomoc", "helpTextCreatePlugin": "Przykładowy tekst o tym, jak stworzyć wtyczkę", + "hidden": "Ukryta", "installedPlugins": "Zainstalowane wtyczki", "intro": "Tutaj znajdziesz dodane przez Ciebie wtyczki TeddyCloud oraz możesz dodać nowe wtyczki udostępnione przez Ciebie lub społeczność TeddyCloud!", "navigationTitle": "Wtyczki TC", @@ -390,10 +392,18 @@ "filter": "Filtruj w bieżącym folderze", "help": { "actionItems": { + "assignToTonie": { + "text": "Przypisuje wybrany plik jako zawartość do Tonie/Taga.", + "title": "Przypisz do Tonie" + }, "deleteFileFolder": { "text": "Usuwa plik lub folder (tylko jeśli folder jest pusty). Uwaga: jeśli ten plik jest przypisany do Tonie/Taga, odwołanie nie zostanie zaktualizowane.", "title": "Usuń plik/folder" }, + "downloadFile": { + "text": "Pobiera wybrany plik lub, w przypadku plików TAF, pobiera go jako plik OGG.", + "title": "Pobierz plik" + }, "duplicateTAPFile": { "text": "Tworzy duplikat wybranego pliku TAP.", "title": "Duplikuj plik TAP" @@ -406,6 +416,16 @@ "text": "Otwiera edytor playlist TeddyAudio do zarządzania plikami TAP.", "title": "Edytuj playlistę TAP" }, + "markAsListened": { + "listened": { + "text": "Plik jest obecnie oznaczony jako odsłuchany. Kliknij, aby oznaczyć go jako nieodsłuchany.", + "title": "Oznacz jako nieodsłuchane" + }, + "unlistened": { + "text": "Plik jest obecnie oznaczony jako nieodsłuchany. Kliknij, aby oznaczyć go jako odsłuchany.", + "title": "Oznacz jako odsłuchane" + } + }, "migrateToAudioIdFolder": { "text": "Przenosi plik TAF do folderu biblioteki 'by/audioId' i aktualizuje źródło w pliku JSON.", "title": "Migruj plik TAF do biblioteki wg AudioId" @@ -425,24 +445,6 @@ "renameFile": { "text": "Zmienia nazwę pliku. Uwaga: jeśli ten plik jest przypisany do Tonie/Taga, odwołanie nie zostanie zaktualizowane automatycznie.", "title": "Zmień nazwę pliku" - }, - "downloadFile": { - "title": "Pobierz plik", - "text": "Pobiera wybrany plik lub, w przypadku plików TAF, pobiera go jako plik OGG." - }, - "markAsListened": { - "unlistened": { - "title": "Oznacz jako odsłuchane", - "text": "Plik jest obecnie oznaczony jako nieodsłuchany. Kliknij, aby oznaczyć go jako odsłuchany." - }, - "listened": { - "title": "Oznacz jako nieodsłuchane", - "text": "Plik jest obecnie oznaczony jako odsłuchany. Kliknij, aby oznaczyć go jako nieodsłuchany." - } - }, - "assignToTonie": { - "title": "Przypisz do Tonie", - "text": "Przypisuje wybrany plik jako zawartość do Tonie/Taga." } }, "actionItemsMulti": { diff --git a/src/components/common/buttons/PluginTemplateDownloadButton.tsx b/src/components/common/buttons/PluginTemplateDownloadButton.tsx index deafddc5..62de66ee 100644 --- a/src/components/common/buttons/PluginTemplateDownloadButton.tsx +++ b/src/components/common/buttons/PluginTemplateDownloadButton.tsx @@ -12,7 +12,8 @@ const createPluginJson = (): string => "standalone": false, "pluginHomepage": "Homepage of the plugin (optionally)", "teddyCloudSection": "one of: home|tonies|tonieboxes|settings|community (optionally)", - "icon": "name of an icon from Ant Design icons (optional)" + "icon": "name of an icon from Ant Design icons (optional)", + "hideInNav": false }`; const createScriptJs = (): string => @@ -430,6 +431,7 @@ You can add more files as needed, for example: - \`pluginHomepage\`: Optional project or documentation URL - \`teddyCloudSection\`: Optional section of the TeddyCloud UI (home|tonies|tonieboxes|settings|community) - \`icon\`: Optional Ant Design icon name +- \`hideInNav\`: Optional boolean flag to hide the plugin in navigation (true/false, no quotes!) --- diff --git a/src/components/community/CommunitySubNav.tsx b/src/components/community/CommunitySubNav.tsx index ef4eec32..5face314 100644 --- a/src/components/community/CommunitySubNav.tsx +++ b/src/components/community/CommunitySubNav.tsx @@ -48,25 +48,27 @@ export const CommunitySubNav = () => { const { plugins } = useTeddyCloud(); - const pluginItems = plugins.map((plugin) => ({ - key: `tcplugins-${plugin.pluginId}`, - label: ( - { - setNavOpen(false); - setSubNavOpen(false); - }} - > - {plugin.pluginName} - - ), - icon: React.createElement(plugin.icon), - title: plugin.pluginName, - })); + const pluginItems = plugins + .filter((p) => !p.hideInNav) + .map((plugin) => ({ + key: `tcplugins-${plugin.pluginId}`, + label: ( + { + setNavOpen(false); + setSubNavOpen(false); + }} + > + {plugin.pluginName} + + ), + icon: React.createElement(plugin.icon), + title: plugin.pluginName, + })); const filteredPluginItems = plugins - .filter((p) => p.teddyCloudSection === TeddyCloudSection.Community) + .filter((p) => p.teddyCloudSection === TeddyCloudSection.Community && !p.hideInNav) .map((plugin) => ({ key: `plugin-${plugin.pluginId}`, label: ( diff --git a/src/components/community/plugincard/PluginCard.tsx b/src/components/community/plugincard/PluginCard.tsx index 94a8b89a..40f3c0c5 100644 --- a/src/components/community/plugincard/PluginCard.tsx +++ b/src/components/community/plugincard/PluginCard.tsx @@ -1,5 +1,5 @@ -import { Card, Typography, Badge, Tooltip, theme } from "antd"; -import { DesktopOutlined, HomeOutlined, DeleteOutlined } from "@ant-design/icons"; +import { Card, Typography, Badge, Tooltip, theme, Tag } from "antd"; +import { DesktopOutlined, HomeOutlined, DeleteOutlined, TagFilled } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import { canHover } from "../../../utils/browser/browserUtils"; import { withBase } from "../../../utils/basePath"; @@ -40,14 +40,40 @@ export const PluginCard: React.FC = ({ const titleContent = plugin.teddyCloudSection ? ( -
-

+
+

{plugin.pluginName}

) : ( -

+

{plugin.pluginName}

); @@ -59,20 +85,24 @@ export const PluginCard: React.FC = ({ key={plugin.pluginId} style={{ width: "100%", - margin: 8, + height: "100%", + minWidth: 0, borderRadius: 8, background: token.colorBgContainerDisabled, display: "flex", flexDirection: "column", - justifyContent: "space-between", + }} + styles={{ + body: { + flex: 1, + }, }} title={
{titleContent} @@ -127,6 +157,13 @@ export const PluginCard: React.FC = ({ e.currentTarget.style.display = "none"; }} /> + {plugin.hideInNav && ( + + + {t("community.plugins.hidden")} + + + )} {plugin.author && `${t("community.plugins.by")} ${plugin.author}`} {plugin.version && ` - v${plugin.version}`} diff --git a/src/components/community/pluginlist/PluginList.tsx b/src/components/community/pluginlist/PluginList.tsx index fa96a0ff..21a9fdf2 100644 --- a/src/components/community/pluginlist/PluginList.tsx +++ b/src/components/community/pluginlist/PluginList.tsx @@ -106,7 +106,7 @@ export const PluginList = () => { {filteredPlugins.length === 0 ? ( ) : ( - + {filteredPlugins.map((plugin) => ( { lg={8} xl={8} xxl={6} + style={{ minWidth: 0, display: "flex" }} > -
- - navigate(`/community/tcplugins/${pluginId}`) - } - onOpenHomepage={(url) => window.open(url, "_blank")} - onDelete={requestDelete} - /> -
+ + navigate(`/community/tcplugins/${pluginId}`) + } + onOpenHomepage={(url) => window.open(url, "_blank")} + onDelete={requestDelete} + /> ))}
diff --git a/src/components/community/pluginlist/modals/PluginHelpModal.tsx b/src/components/community/pluginlist/modals/PluginHelpModal.tsx index ef7230ef..5472aade 100644 --- a/src/components/community/pluginlist/modals/PluginHelpModal.tsx +++ b/src/components/community/pluginlist/modals/PluginHelpModal.tsx @@ -22,6 +22,7 @@ export const PluginHelpModal: React.FC = ({ open, onClose ["pluginHomepage", t("community.plugins.help.fields.pluginHomepage")], ["teddyCloudSection", t("community.plugins.help.fields.teddyCloudSection")], ["icon", t("community.plugins.help.fields.icon")], + ["hideInNav", t("community.plugins.help.fields.hideInNav")], ]; return ( @@ -65,7 +66,8 @@ export const PluginHelpModal: React.FC = ({ open, onClose "version": "Version of the plugin", "pluginHomepage": "Homepage of the plugin", "teddyCloudSection": "tonies", - "icon": "TrophyOutlined" + "icon": "TrophyOutlined", + "hideInNav": false }`} /> diff --git a/src/components/home/HomeSubNav.tsx b/src/components/home/HomeSubNav.tsx index 230806cc..4b77c4d7 100644 --- a/src/components/home/HomeSubNav.tsx +++ b/src/components/home/HomeSubNav.tsx @@ -25,7 +25,7 @@ export const HomeSubNav = () => { }, [currentLanguage]); const pluginItems = plugins - .filter((p) => p.teddyCloudSection === TeddyCloudSection.Home) + .filter((p) => p.teddyCloudSection === TeddyCloudSection.Home && !p.hideInNav) .map((plugin) => ({ key: `plugin-${plugin.pluginId}`, label: ( diff --git a/src/components/settings/SettingsSubNav.tsx b/src/components/settings/SettingsSubNav.tsx index 76d82cdb..87cc81ea 100644 --- a/src/components/settings/SettingsSubNav.tsx +++ b/src/components/settings/SettingsSubNav.tsx @@ -48,7 +48,7 @@ export const SettingsSubNav = () => { }, [currentLanguage]); const pluginItems = plugins - .filter((p) => p.teddyCloudSection === TeddyCloudSection.Settings) + .filter((p) => p.teddyCloudSection === TeddyCloudSection.Settings && !p.hideInNav) .map((plugin) => ({ key: `plugin-${plugin.pluginId}`, label: ( diff --git a/src/components/tonieboxes/TonieboxesSubNav.tsx b/src/components/tonieboxes/TonieboxesSubNav.tsx index 7147b8c4..801395f6 100644 --- a/src/components/tonieboxes/TonieboxesSubNav.tsx +++ b/src/components/tonieboxes/TonieboxesSubNav.tsx @@ -28,7 +28,7 @@ export const TonieboxesSubNav = () => { }, [openKeys, currentLanguage]); const pluginItems = plugins - .filter((p) => p.teddyCloudSection === TeddyCloudSection.Tonieboxes) + .filter((p) => p.teddyCloudSection === TeddyCloudSection.Tonieboxes && !p.hideInNav) .map((plugin) => ({ key: `plugin-${plugin.pluginId}`, label: ( diff --git a/src/components/tonies/ToniesSubNav.tsx b/src/components/tonies/ToniesSubNav.tsx index ca9adbb0..1a5c0c68 100644 --- a/src/components/tonies/ToniesSubNav.tsx +++ b/src/components/tonies/ToniesSubNav.tsx @@ -33,7 +33,7 @@ export const ToniesSubNav = () => { }, [currentLanguage]); const pluginItems = plugins - .filter((p) => p.teddyCloudSection === TeddyCloudSection.Tonies) + .filter((p) => p.teddyCloudSection === TeddyCloudSection.Tonies && !p.hideInNav) .map((plugin) => ({ key: `plugin-${plugin.pluginId}`, label: ( diff --git a/src/provider/TeddyCloudProvider.tsx b/src/provider/TeddyCloudProvider.tsx index c756555e..806f1a50 100644 --- a/src/provider/TeddyCloudProvider.tsx +++ b/src/provider/TeddyCloudProvider.tsx @@ -373,6 +373,7 @@ export function TeddyCloudProvider({ children }: TeddyCloudProviderProps) { ) ? meta.teddyCloudSection : null, + hideInNav: !!meta.hideInNav, icon: (meta.icon && meta.icon in AntIcons ? AntIcons[meta.icon as keyof typeof AntIcons] : AntIcons.CodeSandboxOutlined) as ElementType, diff --git a/src/types/pluginsMetaTypes.ts b/src/types/pluginsMetaTypes.ts index d15658a1..8767e4fd 100644 --- a/src/types/pluginsMetaTypes.ts +++ b/src/types/pluginsMetaTypes.ts @@ -14,6 +14,7 @@ export type PluginMeta = { author: string; version: string; description: string; + hideInNav?: boolean; pluginHomepage?: string; teddyCloudSection?: TeddyCloudSection; icon: ElementType; From fb9e488c143c85b5165a8ce3f0c9549964dc117f Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:32:41 +0200 Subject: [PATCH 02/25] added missing changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc54440a..be7b04bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - gui: Fixed custom Tonie track filtering when `sourceInfo.tracks` is empty but `tonieInfo.tracks` contains track data - gui: Fixed sibling episode assignment for library-root files and improved handling of failed directory requests - gui: Expanded automated test coverage for authentication, API requests and core Tonie/library logic +- gui: Added support for the optional `hideInNav` property in plugin `plugin.json` files to prevent plugins from being shown in the navigation ### Commits From 0f33563e570e969b2adcfa334cff7d9dbe9e5ced Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:42:35 +0200 Subject: [PATCH 03/25] Add hidden plugin filter and count to PluginList --- .../community/pluginlist/PluginList.tsx | 19 ++++++++---- .../pluginlist/hooks/usePluginList.ts | 29 ++++++++++++++----- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/components/community/pluginlist/PluginList.tsx b/src/components/community/pluginlist/PluginList.tsx index 21a9fdf2..00e64932 100644 --- a/src/components/community/pluginlist/PluginList.tsx +++ b/src/components/community/pluginlist/PluginList.tsx @@ -21,6 +21,9 @@ export const PluginList = () => { activeSectionFilters, pluginCountBySection, toggleSectionFilter, + hiddenOnly, + setHiddenOnly, + hiddenPluginCount, isVisibleHelpModal, openHelp, @@ -79,29 +82,33 @@ export const PluginList = () => {

{t("community.plugins.installedPlugins")}

-
+
{allSections.map((section) => { const isChecked = activeSectionFilters.includes(section); + return ( toggleSectionFilter(section, checked)} > - - {section.charAt(0).toUpperCase() + section.slice(1)} - + {section.charAt(0).toUpperCase() + section.slice(1)} ); })} + + + + {t("community.plugins.hidden")} + +
{filteredPlugins.length === 0 ? ( diff --git a/src/components/community/pluginlist/hooks/usePluginList.ts b/src/components/community/pluginlist/hooks/usePluginList.ts index 3670fa5c..51202583 100644 --- a/src/components/community/pluginlist/hooks/usePluginList.ts +++ b/src/components/community/pluginlist/hooks/usePluginList.ts @@ -20,6 +20,12 @@ export const usePluginList = () => { const [file, setFile] = useState(null); const [uploading, setUploading] = useState(false); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); + const [hiddenOnly, setHiddenOnly] = useState(false); + + const hiddenPluginCount = useMemo( + () => plugins.filter((plugin) => plugin.hideInNav).length, + [plugins], + ); const allSections = useMemo( () => @@ -49,14 +55,18 @@ export const usePluginList = () => { [plugins, t], ); - const filteredPlugins = useMemo( - () => - (plugins as TeddyCloudPlugin[]).filter((plugin) => { - const section = plugin.teddyCloudSection || t("community.plugins.filter.unknown"); - return activeSectionFilters.includes(section); - }), - [plugins, activeSectionFilters, t], - ); + const filteredPlugins = useMemo(() => { + return plugins.filter((plugin) => { + const matchesSection = + activeSectionFilters.length === 0 || + (plugin.teddyCloudSection && + activeSectionFilters.includes(plugin.teddyCloudSection)); + + const matchesHidden = !hiddenOnly || plugin.hideInNav; + + return matchesSection && matchesHidden; + }); + }, [plugins, activeSectionFilters, hiddenOnly]); const toggleSectionFilter = (section: string, checked: boolean) => { setActiveSectionFilters((prev) => @@ -166,6 +176,9 @@ export const usePluginList = () => { filteredPlugins, allSections, activeSectionFilters, + hiddenOnly, + setHiddenOnly, + hiddenPluginCount, pluginCountBySection, // filters From e944af9b70582c20d90b56260b42e296780cef80 Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:08:25 +0200 Subject: [PATCH 04/25] Add pagination feature to PluginList and translations for pagination UI --- public/translations/de.json | 4 + public/translations/en.json | 4 + public/translations/es.json | 4 + public/translations/fr.json | 4 + public/translations/it.json | 4 + public/translations/pl.json | 4 + .../community/pluginlist/PluginList.tsx | 174 +++++++++++++++--- .../pagination/PluginPagination.tsx | 42 +++++ 8 files changed, 211 insertions(+), 29 deletions(-) create mode 100644 src/components/community/pluginlist/pagination/PluginPagination.tsx diff --git a/public/translations/de.json b/public/translations/de.json index 5e5574dd..3d5a85cf 100644 --- a/public/translations/de.json +++ b/public/translations/de.json @@ -164,6 +164,10 @@ "navigationTitle": "TC Plugins", "ok": "OK", "open": "Plugin öffnen", + "pagination": { + "pageSelector": "Plugins pro Seite", + "showAll": "Alle anzeigen" + }, "plugin": "Plugin", "title": "TeddyCloud Plugins", "upload": { diff --git a/public/translations/en.json b/public/translations/en.json index 41241e66..7e511c81 100644 --- a/public/translations/en.json +++ b/public/translations/en.json @@ -164,6 +164,10 @@ "navigationTitle": "TC Plugins", "ok": "OK", "open": "Open Plugin", + "pagination": { + "pageSelector": "Plugins per Page", + "showAll": "Show All" + }, "plugin": "Plugin", "title": "TeddyCloud Plugins", "upload": { diff --git a/public/translations/es.json b/public/translations/es.json index abd10683..738e1fcc 100644 --- a/public/translations/es.json +++ b/public/translations/es.json @@ -165,6 +165,10 @@ "ok": "OK", "open": "Abrir plugin", "plugin": "Plugin", + "pagination": { + "pageSelector": "Plugins por página", + "showAll": "Mostrar todos" + }, "title": "Plugins de TeddyCloud", "upload": { "add": "Agregar plugin", diff --git a/public/translations/fr.json b/public/translations/fr.json index 1a1f646e..ea023e53 100644 --- a/public/translations/fr.json +++ b/public/translations/fr.json @@ -165,6 +165,10 @@ "ok": "OK", "open": "Ouvrir le plugin", "plugin": "Plugin", + "pagination": { + "pageSelector": "Plugins par page", + "showAll": "Afficher tout" + }, "title": "Plugins TeddyCloud", "upload": { "add": "Ajouter un plugin", diff --git a/public/translations/it.json b/public/translations/it.json index e21c595f..7c38f37a 100644 --- a/public/translations/it.json +++ b/public/translations/it.json @@ -165,6 +165,10 @@ "ok": "OK", "open": "Apri plugin", "plugin": "Plugin", + "pagination": { + "pageSelector": "Plugins per pagina", + "showAll": "Mostra tutti" + }, "title": "Plugin TeddyCloud", "upload": { "add": "Aggiungi plugin", diff --git a/public/translations/pl.json b/public/translations/pl.json index c80f5a34..99275c66 100644 --- a/public/translations/pl.json +++ b/public/translations/pl.json @@ -165,6 +165,10 @@ "ok": "OK", "open": "Otwórz wtyczkę", "plugin": "Wtyczka", + "pagination": { + "pageSelector": "Wtyczki na stronę", + "showAll": "Pokaż wszystkie" + }, "title": "Wtyczki TeddyCloud", "upload": { "add": "Dodaj wtyczkę", diff --git a/src/components/community/pluginlist/PluginList.tsx b/src/components/community/pluginlist/PluginList.tsx index 00e64932..48438820 100644 --- a/src/components/community/pluginlist/PluginList.tsx +++ b/src/components/community/pluginlist/PluginList.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { Alert, Badge, Button, Col, Empty, Row, Tag, Typography } from "antd"; @@ -8,8 +9,12 @@ import { PluginCard } from "../plugincard/PluginCard"; import { PluginDeleteDialog } from "./modals/PluginDeleteModal"; import { PluginHelpModal } from "./modals/PluginHelpModal"; import { PluginUploadModal } from "./modals/PluginUploadModal"; +import PluginPagination from "./pagination/PluginPagination"; +import { scrollToTop } from "../../../utils/browser/browserUtils"; +import { userStorage } from "../../../utils/storage/userStorage"; const { Paragraph } = Typography; +const STORAGE_KEY = "pluginListState"; export const PluginList = () => { const { t } = useTranslation(); @@ -44,6 +49,106 @@ export const PluginList = () => { handleCancelDelete, } = usePluginList(); + const [pageSize, setPageSize] = useState(() => { + const storedState = userStorage.getItem(STORAGE_KEY); + if (storedState) { + try { + const { pageSize: storedPageSize } = JSON.parse(storedState); + if (typeof storedPageSize === "number") return storedPageSize; + } catch (error) { + console.error("Error parsing stored plugin list state:", error); + } + } + return 24; + }); + const [currentPage, setCurrentPage] = useState(1); + const [paginationEnabled, setPaginationEnabled] = useState(true); + const [showAll, setShowAll] = useState(false); + const pluginListRef = useRef(null); + + useEffect(() => { + const storedState = userStorage.getItem(STORAGE_KEY); + if (storedState) { + try { + const { pageSize: storedPageSize, showAll: storedShowAll } = + JSON.parse(storedState); + if (storedShowAll) { + setPageSize(storedPageSize); + handleShowAll(storedPageSize); + } else { + setPageSize(storedPageSize); + handlePageSizeChange(1, storedPageSize); + } + } catch (error) { + console.error("Error parsing stored plugin list state:", error); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const stateToStore = JSON.stringify({ + pageSize, + paginationEnabled, + showAll, + }); + userStorage.setItem(STORAGE_KEY, stateToStore); + }, [pageSize, paginationEnabled, showAll]); + + useEffect(() => { + handlePageSizeChange(1, pageSize); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageSize]); + + const handleShowAll = (size?: number) => { + const effectiveSize = size ?? pageSize; + setPageSize(effectiveSize); + setShowAll(true); + setPaginationEnabled(false); + }; + + const handleShowPagination = () => { + setPaginationEnabled(true); + setShowAll(false); + handlePageSizeChange(1, pageSize); + }; + + const handlePageSizeChange = (current: number, size: number) => { + setPageSize(size); + setCurrentPage(current); + setTimeout(() => scrollToTop(pluginListRef.current), 0); + }; + + const handleSectionFilterChange = (section: string, checked: boolean) => { + toggleSectionFilter(section, checked); + setCurrentPage(1); + }; + + const handleHiddenFilterChange = (checked: boolean) => { + setHiddenOnly(checked); + setCurrentPage(1); + }; + + const currentPageData = showAll + ? filteredPlugins + : filteredPlugins.slice((currentPage - 1) * pageSize, currentPage * pageSize); + + const listPagination = ( +
+ {!paginationEnabled ? ( + + ) : ( + handleShowAll()} + /> + )} +
+ ); + return ( <>

{t("community.plugins.title")}

@@ -96,7 +201,9 @@ export const PluginList = () => { > toggleSectionFilter(section, checked)} + onChange={(checked) => + handleSectionFilterChange(section, checked) + } > {section.charAt(0).toUpperCase() + section.slice(1)} @@ -105,38 +212,47 @@ export const PluginList = () => { })} - + {t("community.plugins.hidden")}
- {filteredPlugins.length === 0 ? ( - - ) : ( - - {filteredPlugins.map((plugin) => ( - - - navigate(`/community/tcplugins/${pluginId}`) - } - onOpenHomepage={(url) => window.open(url, "_blank")} - onDelete={requestDelete} - /> - - ))} - - )} + +
+ {filteredPlugins.length === 0 ? ( + + ) : ( + <> + {listPagination} + + + {currentPageData.map((plugin) => ( + + + navigate(`/community/tcplugins/${pluginId}`) + } + onOpenHomepage={(url) => window.open(url, "_blank")} + onDelete={requestDelete} + /> + + ))} + + + {listPagination} + + )} +
void; + total: number; + pageSize: number; + additionalButtonOnClick: () => void; +} + +const PluginPagination: React.FC = ({ + currentPage, + onChange, + total, + pageSize, + additionalButtonOnClick, +}) => { + const { t } = useTranslation(); + + return ( + <> + + + + ); +}; + +export default PluginPagination; From 738334774dc2f03ec869d7692ade50bf96885973 Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:24:32 +0200 Subject: [PATCH 05/25] Refactor pagination button styles in PluginList and ToniesList components --- src/components/community/pluginlist/PluginList.tsx | 8 +++++--- .../community/pluginlist/pagination/PluginPagination.tsx | 5 +++-- src/components/tonies/tonieslist/ToniesList.tsx | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/community/pluginlist/PluginList.tsx b/src/components/community/pluginlist/PluginList.tsx index 48438820..c4a0e11f 100644 --- a/src/components/community/pluginlist/PluginList.tsx +++ b/src/components/community/pluginlist/PluginList.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { Alert, Badge, Button, Col, Empty, Row, Tag, Typography } from "antd"; +import { Alert, Badge, Button, Col, Empty, Flex, Row, Tag, Typography } from "antd"; import { AppstoreAddOutlined, QuestionCircleOutlined } from "@ant-design/icons"; import { usePluginList } from "./hooks/usePluginList"; import PluginTemplateDownloadButton from "../../common/buttons/PluginTemplateDownloadButton"; @@ -59,7 +59,7 @@ export const PluginList = () => { console.error("Error parsing stored plugin list state:", error); } } - return 24; + return 12; }); const [currentPage, setCurrentPage] = useState(1); const [paginationEnabled, setPaginationEnabled] = useState(true); @@ -136,7 +136,9 @@ export const PluginList = () => { const listPagination = (
{!paginationEnabled ? ( - + ) : ( = ({ pageSize={pageSize} onChange={onChange} showSizeChanger - pageSizeOptions={["12", "24", "48", "96", "192"]} + pageSizeOptions={["12", "24", "48"]} locale={{ items_per_page: t("community.plugins.pagination.pageSelector") }} - style={{ marginBottom: 0 }} + style={{ marginBottom: 8 }} + styles={{ item: { padding: 0 } }} showLessItems /> + ) : ( Date: Fri, 9 Oct 2026 20:25:16 +0200 Subject: [PATCH 06/25] added changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be7b04bf..0f35a0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - gui: Fixed sibling episode assignment for library-root files and improved handling of failed directory requests - gui: Expanded automated test coverage for authentication, API requests and core Tonie/library logic - gui: Added support for the optional `hideInNav` property in plugin `plugin.json` files to prevent plugins from being shown in the navigation +- gui: Added pagination in plugin list ### Commits From 2632981f0535f2edf0926d5bdbfc569b39ccbb0c Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:48:49 +0200 Subject: [PATCH 07/25] Refactor PluginCard to remove unnecessary strong tags and update PluginList pagination options --- .../community/plugincard/PluginCard.tsx | 4 +- .../community/pluginlist/PluginList.tsx | 52 ++++++++++++------- .../pagination/PluginPagination.tsx | 3 +- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/components/community/plugincard/PluginCard.tsx b/src/components/community/plugincard/PluginCard.tsx index 40f3c0c5..79409546 100644 --- a/src/components/community/plugincard/PluginCard.tsx +++ b/src/components/community/plugincard/PluginCard.tsx @@ -58,7 +58,7 @@ export const PluginCard: React.FC = ({ whiteSpace: "nowrap", }} > - {plugin.pluginName} + {plugin.pluginName}
@@ -74,7 +74,7 @@ export const PluginCard: React.FC = ({ whiteSpace: "nowrap", }} > - {plugin.pluginName} + {plugin.pluginName} ); diff --git a/src/components/community/pluginlist/PluginList.tsx b/src/components/community/pluginlist/PluginList.tsx index c4a0e11f..9337a6e9 100644 --- a/src/components/community/pluginlist/PluginList.tsx +++ b/src/components/community/pluginlist/PluginList.tsx @@ -10,7 +10,6 @@ import { PluginDeleteDialog } from "./modals/PluginDeleteModal"; import { PluginHelpModal } from "./modals/PluginHelpModal"; import { PluginUploadModal } from "./modals/PluginUploadModal"; import PluginPagination from "./pagination/PluginPagination"; -import { scrollToTop } from "../../../utils/browser/browserUtils"; import { userStorage } from "../../../utils/storage/userStorage"; const { Paragraph } = Typography; @@ -74,10 +73,11 @@ export const PluginList = () => { JSON.parse(storedState); if (storedShowAll) { setPageSize(storedPageSize); - handleShowAll(storedPageSize); + setShowAll(true); + setPaginationEnabled(false); } else { setPageSize(storedPageSize); - handlePageSizeChange(1, storedPageSize); + setCurrentPage(1); } } catch (error) { console.error("Error parsing stored plugin list state:", error); @@ -95,11 +95,6 @@ export const PluginList = () => { userStorage.setItem(STORAGE_KEY, stateToStore); }, [pageSize, paginationEnabled, showAll]); - useEffect(() => { - handlePageSizeChange(1, pageSize); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageSize]); - const handleShowAll = (size?: number) => { const effectiveSize = size ?? pageSize; setPageSize(effectiveSize); @@ -116,7 +111,7 @@ export const PluginList = () => { const handlePageSizeChange = (current: number, size: number) => { setPageSize(size); setCurrentPage(current); - setTimeout(() => scrollToTop(pluginListRef.current), 0); + setTimeout(scrollToPluginList, 0); }; const handleSectionFilterChange = (section: string, checked: boolean) => { @@ -129,6 +124,23 @@ export const PluginList = () => { setCurrentPage(1); }; + const scrollToPluginList = () => { + const element = pluginListRef.current; + + if (!element) { + return; + } + + const rect = element.getBoundingClientRect(); + + if (rect.top < 0) { + window.scrollTo({ + top: window.scrollY + rect.top - 16, + behavior: "smooth", + }); + } + }; + const currentPageData = showAll ? filteredPlugins : filteredPlugins.slice((currentPage - 1) * pageSize, currentPage * pageSize); @@ -154,12 +166,6 @@ export const PluginList = () => { return ( <>

{t("community.plugins.title")}

-
{
- + + <>

{t("community.plugins.installedPlugins")}

{allSections.map((section) => { @@ -220,14 +232,14 @@ export const PluginList = () => {
-
+ {filteredPlugins.length === 0 ? ( ) : ( <> {listPagination} - + {currentPageData.map((plugin) => ( { {listPagination} )} -
+ { onConfirm={handleConfirmDelete} onCancel={handleCancelDelete} /> -
+ ); }; diff --git a/src/components/community/pluginlist/pagination/PluginPagination.tsx b/src/components/community/pluginlist/pagination/PluginPagination.tsx index 10fb739f..86302398 100644 --- a/src/components/community/pluginlist/pagination/PluginPagination.tsx +++ b/src/components/community/pluginlist/pagination/PluginPagination.tsx @@ -27,10 +27,9 @@ const PluginPagination: React.FC = ({ pageSize={pageSize} onChange={onChange} showSizeChanger - pageSizeOptions={["12", "24", "48"]} + pageSizeOptions={["6", "12", "24", "48"]} locale={{ items_per_page: t("community.plugins.pagination.pageSelector") }} style={{ marginBottom: 8 }} - styles={{ item: { padding: 0 } }} showLessItems /> - ); - }; - const sanitizeHostname = (input: string) => input.replace(/[^a-zA-Z0-9-.]/g, "").trim(); const handleHostnameChange = (value: string) => { @@ -101,6 +71,7 @@ export const CC3200BoxFlashingGuide: React.FC = () => { const portNumber = Number(port); const isPortInvalid = !isValidAltPort(portNumber) || portNumber === 443; const { createPatch: createPortPatch } = useAltPortCustomPatch(portNumber); + const { createPatch: createHostNamePatch } = useAltUrlCustomPatch(hostname); const prev = () => setCurrentStep((s) => Math.max(0, s - 1)); const next = () => setCurrentStep((s) => Math.min(steps.length - 1, s + 1)); @@ -132,6 +103,8 @@ export const CC3200BoxFlashingGuide: React.FC = () => { hostname={hostname} warningTextHostname={warningTextHostname} onHostnameChange={handleHostnameChange} + onCreateHostNamePatch={createHostNamePatch} + isHostnameInvalid={isHostnameInvalid} port={port} warningTextPort={warningTextPort} onPortChange={handlePortChange} @@ -257,12 +230,7 @@ export const CC3200BoxFlashingGuide: React.FC = () => { }} > {previousButton} -
- -
+
+ {warningTextHostname && (

{warningTextHostname}

From 2c3830d832da0a7199d835e2f0228ff4d29baa6d Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:10:24 +0200 Subject: [PATCH 16/25] fix(translations): update hintPatchFlash messages to clarify hostname and port adjustments --- public/translations/de.json | 2 +- public/translations/en.json | 2 +- public/translations/es.json | 2 +- public/translations/fr.json | 2 +- public/translations/it.json | 2 +- public/translations/pl.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/translations/de.json b/public/translations/de.json index 7e9d36a6..839c7e56 100644 --- a/public/translations/de.json +++ b/public/translations/de.json @@ -1762,7 +1762,7 @@ "hintESP32FirmwareFlashed": "Das Patchen ist abgeschlossen. Trenne die Toniebox vom UART, entferne den Kurzschluss von J100 und starte die Box neu. Sie sollte sich jetzt mit TeddyCloud verbinden können!", "hintESP32FirmwareReseted": "Das Zurücksetzen sollte abgeschlossen sein. Bitte trenne die Toniebox vom UART, entferne den Kurzschluss von J100 und starte die Box neu. Sie sollte nicht mehr mit deinem Teddycloud verbunden sein. Führe einen Frische-Check durch, um zu testen, ob alles einwandfrei funktioniert.", "hintFlashESP32RevvoxFlasher": "Klicke auf „ESP32 flashen“, wähle falls nötig den COM-Port aus und warte, bis der Schritt abgeschlossen ist!", - "hintPatchFlash": "Um das ausgewählte / gelesene Flash mit dem dem CA-Zertifikat deiner TeddyCloud zu versehen und ggf. weitere Anpassungen wie Hostname der TeddyCloud und/oder der WiFi-Zugangsdaten vorzunehmen, klicke nach Eingabe auf \"Patch\" oder gehe ohne Patching direkt zum nächsten Schritt. ", + "hintPatchFlash": "Um das ausgewählte / gelesene Flash mit dem dem CA-Zertifikat deiner TeddyCloud zu versehen und ggf. weitere Anpassungen wie Hostname und Port der TeddyCloud und/oder der WiFi-Zugangsdaten vorzunehmen, klicke nach Eingabe auf \"Patch\" oder gehe ohne Patching direkt zum nächsten Schritt. ", "hintPatchHost": "Gib den gewünschten Hostnamen deiner TeddyCloud-Instanz ein.", "hintPatchPort": "Port, mit dem sich die Box verbindet. Lass 443 stehen, außer die Box soll TeddyCloud über einen anderen Port erreichen, z. B. über einen Proxy, der nach Port verteilt. CC3235-Boxen und die Toniebox 2 lassen sich nicht patchen: Sie verbinden sich immer mit Port 443.", "hintPatchWifi": "Gib optional die WiFi Zugangsdaten für deine Toniebox ein. Bitte nur setzen, wenn es wirklich nötig ist - normalerweise, wenn deine Toniebox kurz vor dem Patchen mit deinem WLAN verbunden war, ist dies nicht erforderlich.", diff --git a/public/translations/en.json b/public/translations/en.json index 755712cd..d37271b4 100644 --- a/public/translations/en.json +++ b/public/translations/en.json @@ -1762,7 +1762,7 @@ "hintESP32FirmwareFlashed": "The patching should be done. Please disconnect the Toniebox from the UART, remove the short circuit from J100 and restart the box. It should now be able to connect to TeddyCloud!", "hintESP32FirmwareReseted": "The reset should be done. Please disconnect the Toniebox from the UART, remove the short circuit from J100 and restart the box. It should be no longer connected to your Teddycloud. Do a freshnesscheck to test if it works fine.", "hintFlashESP32RevvoxFlasher": "Click on \"Flash ESP32\", if necessary choose the COM-Port and wait till the step is done!", - "hintPatchFlash": "To apply the selected/read flash with your TeddyCloud CA certificate and possibly make other adjustments like the TeddyCloud hostname and/or WiFi credentials, click 'Patch' after entering the information or proceed to the next step without patching.", + "hintPatchFlash": "To apply the selected/read flash with your TeddyCloud CA certificate and possibly make other adjustments like the TeddyCloud hostname, port and/or WiFi credentials, click 'Patch' after entering the information or proceed to the next step without patching.", "hintPatchHost": "Enter the desired hostname for your TeddyCloud instance.", "hintPatchPort": "Port the box connects to. Keep 443 unless the box should reach TeddyCloud on another port, e.g. through a proxy that routes by port. CC3235 boxes and the Toniebox 2 cannot be patched: they always connect to port 443.", "hintPatchWifi": "Optionally enter the WiFi credentials for your Toniebox. Please only set this if you really need to - usually, if your Toniebox was connected to your WiFi right before patching, you don't need to.", diff --git a/public/translations/es.json b/public/translations/es.json index 025cb7ce..da3a7be1 100644 --- a/public/translations/es.json +++ b/public/translations/es.json @@ -1762,7 +1762,7 @@ "hintESP32FirmwareFlashed": "El parcheo debería estar hecho. Desconecta la Toniebox del UART, quita el cortocircuito de J100 y reinicia la caja. ¡Ahora debería poder conectarse a TeddyCloud!", "hintESP32FirmwareReseted": "El restablecimiento debería estar completado. Por favor, desconecta la Toniebox del UART, retira el cortocircuito de J100 y reinicia la caja. Ya no debería estar conectada a tu Teddycloud. Realiza una comprobación de frescura para verificar que todo funciona bien.", "hintFlashESP32RevvoxFlasher": "Haz clic en \"Flashear ESP32\", elige el puerto COM si es necesario y espera a que el paso termine.", - "hintPatchFlash": "Para aplicar el flash seleccionado/leído con tu certificado CA de TeddyCloud y posiblemente hacer otros ajustes como el nombre de host de TeddyCloud y/o credenciales WiFi, haz clic en 'Parchear' después de ingresar la información o continúa con el siguiente paso sin parchear.", + "hintPatchFlash": "Para aplicar el flash seleccionado/leído con tu certificado CA de TeddyCloud y posiblemente hacer otros ajustes como el nombre de host y el puerto de TeddyCloud y/o credenciales WiFi, haz clic en 'Parchear' después de ingresar la información o continúa con el siguiente paso sin parchear.", "hintPatchHost": "Introduce el nombre de host deseado para tu instancia de TeddyCloud.", "hintPatchPort": "Puerto al que se conecta la Toniebox. Deja 443 salvo que la Toniebox deba conectarse a TeddyCloud a través de otro puerto, por ejemplo mediante un proxy que enruta por puerto. Las Toniebox CC3235 y la Toniebox 2 no se pueden parchear: siempre se conectan al puerto 443.", "hintPatchWifi": "Opcionalmente ingresa las credenciales WiFi para tu Toniebox. Por favor, solo configúralo si realmente es necesario; normalmente, si tu Toniebox estaba conectada a tu WiFi justo antes de aplicar el parche, no necesitas hacerlo.", diff --git a/public/translations/fr.json b/public/translations/fr.json index c5f79d3b..51874120 100644 --- a/public/translations/fr.json +++ b/public/translations/fr.json @@ -1762,7 +1762,7 @@ "hintESP32FirmwareFlashed": "Le patchage est terminé. Déconnecte la Toniebox de l'UART, retire le court-circuit du J100 et redémarre la box. Elle devrait maintenant pouvoir se connecter à TeddyCloud !", "hintESP32FirmwareReseted": "La réinitialisation devrait être terminée. Veuillez déconnecter la Toniebox du UART, retirer le court-circuit de J100 et redémarrer la box. Elle ne devrait plus être connectée à ton Teddycloud. Fais une vérification de fraîcheur pour tester si tout fonctionne correctement.", "hintFlashESP32RevvoxFlasher": "Clique sur « Flasher l'ESP32 », choisis le port COM si nécessaire et attends que l'étape soit terminée.", - "hintPatchFlash": "Pour appliquer le flash sélectionné/lu avec le certificat CA de votre TeddyCloud et éventuellement effectuer d'autres ajustements tels que le nom d'hôte de TeddyCloud et/ou les identifiants WiFi, cliquez sur 'Patch' après avoir entré les informations ou passez à l'étape suivante sans patch.", + "hintPatchFlash": "Pour appliquer le flash sélectionné/lu avec le certificat CA de votre TeddyCloud et éventuellement effectuer d'autres ajustements tels que le nom d'hôte et le port de TeddyCloud et/ou les identifiants WiFi, cliquez sur 'Patch' après avoir entré les informations ou passez à l'étape suivante sans patch.", "hintPatchHost": "Entrez le nom d'hôte souhaité pour votre instance TeddyCloud.", "hintPatchPort": "Port auquel la Toniebox se connecte. Garde 443, sauf si la Toniebox doit joindre TeddyCloud sur un autre port, par exemple via un proxy qui route selon le port. Les Toniebox CC3235 et la Toniebox 2 ne peuvent pas être patchées : elles se connectent toujours au port 443.", "hintPatchWifi": "Entrez optionnellement les identifiants WiFi pour votre Toniebox. Merci de ne le faire que si c'est vraiment nécessaire - normalement, si votre Toniebox était connectée à votre WiFi juste avant la mise à jour, ce n'est pas nécessaire.", diff --git a/public/translations/it.json b/public/translations/it.json index ff16efe8..637598c7 100644 --- a/public/translations/it.json +++ b/public/translations/it.json @@ -1762,7 +1762,7 @@ "hintESP32FirmwareFlashed": "L'applicazione delle patch dovrebbe essere completata. Scollega il Toniebox dalla UART, rimuovi il cortocircuito da J100 e riavvia il box. Ora dovrebbe potersi connettere a TeddyCloud!", "hintESP32FirmwareReseted": "Il ripristino dovrebbe essere completato. Scollega il Toniebox dalla UART, rimuovi il cortocircuito da J100 e riavvia il box. Non dovrebbe più essere collegato a TeddyCloud. Esegui un freshness check per verificare che funzioni correttamente.", "hintFlashESP32RevvoxFlasher": "Fai clic su \"Flash ESP32\", se necessario scegli la porta COM e attendi il completamento del passaggio!", - "hintPatchFlash": "Per applicare alla flash selezionata/letta il certificato CA TeddyCloud ed eventualmente altre modifiche, come l'hostname di TeddyCloud e/o le credenziali Wi-Fi, fai clic su 'Patch' dopo aver inserito le informazioni oppure passa al passaggio successivo senza applicare patch.", + "hintPatchFlash": "Per applicare alla flash selezionata/letta il certificato CA TeddyCloud ed eventualmente altre modifiche, come l'hostname e il port di TeddyCloud e/o le credenziali Wi-Fi, fai clic su 'Patch' dopo aver inserito le informazioni oppure passa al passaggio successivo senza applicare patch.", "hintPatchHost": "Inserisci l'hostname desiderato per la tua istanza TeddyCloud.", "hintPatchPort": "Porta a cui si connette la Toniebox. Lascia 443, a meno che la Toniebox non debba raggiungere TeddyCloud su un'altra porta, ad esempio tramite un proxy che instrada in base alla porta. Le Toniebox CC3235 e la Toniebox 2 non si possono patchare: si connettono sempre alla porta 443.", "hintPatchWifi": "Facoltativamente inserisci le credenziali Wi-Fi del Toniebox. Impostale solo se realmente necessario: di solito, se il Toniebox era collegato al Wi-Fi subito prima dell'applicazione delle patch, non serve.", diff --git a/public/translations/pl.json b/public/translations/pl.json index 0411e6af..d2a0165b 100644 --- a/public/translations/pl.json +++ b/public/translations/pl.json @@ -1762,7 +1762,7 @@ "hintESP32FirmwareFlashed": "Patchowanie powinno być zakończone. Odłącz Tonieboxa od UART, usuń zwarcie z J100 i zrestartuj boxa. Powinien teraz móc połączyć się z TeddyCloud!", "hintESP32FirmwareReseted": "Reset powinien być zakończony. Odłącz Tonieboxa od UART, usuń zwarcie z J100 i zrestartuj boxa. Nie powinien już być połączony z Twoim Teddycloud. Wykonaj freshnesscheck, aby sprawdzić, czy działa poprawnie.", "hintFlashESP32RevvoxFlasher": "Kliknij \"Flash ESP32\", w razie potrzeby wybierz port COM i poczekaj, aż krok się zakończy!", - "hintPatchFlash": "Aby zastosować wybrany/odczytany flash z certyfikatem CA TeddyCloud i ewentualnie wprowadzić inne zmiany, takie jak nazwa hosta TeddyCloud i/lub dane Wi-Fi, kliknij 'Patch' po wprowadzeniu informacji lub przejdź do następnego kroku bez patchowania.", + "hintPatchFlash": "Aby zastosować wybrany/odczytany flash z certyfikatem CA TeddyCloud i ewentualnie wprowadzić inne zmiany, takie jak nazwa hosta i port TeddyCloud i/lub dane Wi-Fi, kliknij 'Patch' po wprowadzeniu informacji lub przejdź do następnego kroku bez patchowania.", "hintPatchHost": "Wprowadź żądaną nazwę hosta dla swojej instancji TeddyCloud.", "hintPatchPort": "Port, z którym łączy się Toniebox. Pozostaw 443, chyba że Toniebox ma łączyć się z TeddyCloud przez inny port, np. przez proxy kierujące ruch według portu. Toniebox z CC3235 i Toniebox 2 nie można załatać: zawsze łączą się z portem 443.", "hintPatchWifi": "Opcjonalnie wprowadź dane Wi-Fi dla swojego Tonieboxa. Ustaw to tylko, jeśli rzeczywiście musisz - zazwyczaj, jeśli Twój Toniebox był podłączony do Twojej sieci Wi-Fi bezpośrednio przed patchowaniem, nie jest to konieczne.", From 0431fa4b17b7f76af525ab372499a87b313d169a Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:40:45 +0200 Subject: [PATCH 17/25] feat: enhance download functionality with loading indicators for whole and selected file downloads --- .../common/modals/TafTrackDownloadModal.tsx | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/components/tonies/common/modals/TafTrackDownloadModal.tsx b/src/components/tonies/common/modals/TafTrackDownloadModal.tsx index ff65faa4..f1d07cee 100644 --- a/src/components/tonies/common/modals/TafTrackDownloadModal.tsx +++ b/src/components/tonies/common/modals/TafTrackDownloadModal.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, Checkbox, Flex, Modal, Table, theme } from "antd"; import type { ColumnsType } from "antd/es/table"; -import { DownloadOutlined, PlayCircleOutlined } from "@ant-design/icons"; +import { DownloadOutlined, LoadingOutlined, PlayCircleOutlined } from "@ant-design/icons"; import styled from "styled-components"; import { @@ -74,6 +74,8 @@ export const TafDownloadPanel: React.FC = ({ const canSplit = tracks.length > 1; const allKeys = useMemo(() => tracks.map((track) => track.number), [tracks]); const [selectedKeys, setSelectedKeys] = useState([]); + const [downloadingWholeFile, setDownloadingWholeFile] = useState(false); + const [downloadingSelected, setDownloadingSelected] = useState(false); useEffect(() => { setSelectedKeys([]); @@ -81,11 +83,24 @@ export const TafDownloadPanel: React.FC = ({ const downloadBase = sanitizeDownloadName(baseFilename || "download"); - const downloadWholeFile = () => { - triggerBrowserDownload(buildTafDownloadUrl(contentUrl), `${downloadBase}.ogg`); + const downloadWholeFile = async () => { + if (downloadingWholeFile) { + return; + } + + setDownloadingWholeFile(true); + try { + await triggerBrowserDownload(buildTafDownloadUrl(contentUrl), `${downloadBase}.ogg`); + } finally { + setDownloadingWholeFile(false); + } }; - const downloadSelected = () => { + const downloadSelected = async () => { + if (downloadingSelected) { + return; + } + const selected = tracks.filter((track) => selectedKeys.includes(track.number)); if (selected.length === 0) { return; @@ -96,12 +111,18 @@ export const TafDownloadPanel: React.FC = ({ return `${padTrackNumber(track.number)} ${sanitizeDownloadName(title)}.ogg`; }); const filename = selected.length === 1 ? entries[0] : `${downloadBase}.zip`; - triggerBrowserDownload( - buildTafDownloadUrl(contentUrl, { - tracks: selected.map((track) => track.number), - }), - filename, - ); + + setDownloadingSelected(true); + try { + await triggerBrowserDownload( + buildTafDownloadUrl(contentUrl, { + tracks: selected.map((track) => track.number), + }), + filename, + ); + } finally { + setDownloadingSelected(false); + } }; if (!canSplit) { @@ -121,7 +142,8 @@ export const TafDownloadPanel: React.FC = ({ ))} +
+ ) : isBackgroundDownloading ? ( + ) : ( ) diff --git a/src/components/tonies/toniecard/hooks/useTonieCardActions.ts b/src/components/tonies/toniecard/hooks/useTonieCardActions.ts index 1db960ca..dc507216 100644 --- a/src/components/tonies/toniecard/hooks/useTonieCardActions.ts +++ b/src/components/tonies/toniecard/hooks/useTonieCardActions.ts @@ -1,3 +1,4 @@ +import { useState } from "react"; import { defaultAPIConfig } from "../../../../config/defaultApiConfig"; import { TeddyCloudApi } from "../../../../api"; import { NotificationTypeEnum } from "../../../../types/teddyCloudNotificationTypes"; @@ -31,6 +32,8 @@ export const useTonieCardActions = ({ closeLoadingNotification, fetchUpdatedTonieCard, }: UseTonieCardActionsParams) => { + const [isBackgroundDownloading, setIsBackgroundDownloading] = useState(false); + const notifySuccess = (message: string, description: string) => { addNotification(NotificationTypeEnum.Success, message, description, t("tonies.title")); }; @@ -122,9 +125,10 @@ export const useTonieCardActions = ({ const handleBackgroundDownload = async () => { const path = tonieCard.downloadTriggerUrl; - if (!path) return; + if (!path || isBackgroundDownloading) return; const key = "loading" + tonieCard.ruid; + setIsBackgroundDownloading(true); try { addLoadingNotification( @@ -159,6 +163,8 @@ export const useTonieCardActions = ({ }).replace(' "" ', ""), error, ); + } finally { + setIsBackgroundDownloading(false); } }; @@ -166,5 +172,6 @@ export const useTonieCardActions = ({ handleLiveClick, handleNoCloudClick, handleBackgroundDownload, + isBackgroundDownloading, }; }; From 86731980ba5cd7d7b59ca9caf33ee56e4f6829ac Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Sun, 18 Oct 2026 10:32:11 +0200 Subject: [PATCH 19/25] feat: update audio play handling to show player display on play and hide on end --- src/components/common/footer/AudioPlayerFooter.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/common/footer/AudioPlayerFooter.tsx b/src/components/common/footer/AudioPlayerFooter.tsx index 09bf71f6..c7b855ec 100644 --- a/src/components/common/footer/AudioPlayerFooter.tsx +++ b/src/components/common/footer/AudioPlayerFooter.tsx @@ -192,7 +192,10 @@ const AudioPlayerFooter: React.FC = ({ onVisibilityChang setVolume(lastVolume); }; - const handleAudioPlay = () => setIsPlaying(true); + const handleAudioPlay = () => { + setIsPlaying(true); + setAudioPlayerDisplay("flex"); + }; const handleAudioPause = () => setIsPlaying(false); const handleAudioEnded = () => setIsPlaying(false); @@ -207,6 +210,7 @@ const AudioPlayerFooter: React.FC = ({ onVisibilityChang } globalAudio.load(); } + setIsPlaying(false); setAudioPlayerDisplay("none"); onVisibilityChange(false); }; From a29e265be0509659a7697345f68b95314fae0559 Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:27:15 +0200 Subject: [PATCH 20/25] feat: refactor file download handling to improve query parameter construction and file name generation --- CHANGELOG.md | 1 + .../filebrowser/hooks/useFileDownload.ts | 38 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90da759c..0cfff02e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ - gui: Track names in the Custom Model Editor keep their position, unnamed tracks in between are no longer dropped - gui: Box setup can create a port patch for CC3200 boxes (altPort.custom.json) and patch the port of ESP32 boxes, e.g. for a proxy that routes boxes by port (not possible for CC3235 boxes and the Toniebox 2) - gui: Show a loading spinner and prevent repeated clicks while a TonieCard background download is in progress +- gui: fix filedownload in content. 500304E0.json files were downloaded as invalid ogg files. ### Commits diff --git a/src/components/tonies/filebrowser/hooks/useFileDownload.ts b/src/components/tonies/filebrowser/hooks/useFileDownload.ts index 85fe7499..9613f1d3 100644 --- a/src/components/tonies/filebrowser/hooks/useFileDownload.ts +++ b/src/components/tonies/filebrowser/hooks/useFileDownload.ts @@ -22,22 +22,32 @@ export function useFileDownload({ setDownloading }: UseFileDownloadParams) { special: string, overlay?: string, ) => { + console.log("handleFileDownload", record, path, special, overlay); + const params = new URLSearchParams(); + if (record.name.toLowerCase().endsWith(".taf")) { + params.set("ogg", "true"); + } + if (special) { + params.set("special", special); + } + if (overlay) { + params.set("overlay", overlay); + } + const query = params.toString(); + const fileUrl = encodeURI("/content/" + decodeURIComponent(path) + "/" + record.name) + - "?" + - (record.name.toLowerCase().endsWith(".taf") ? "ogg=true&" : "") + - "special=" + - special + - (overlay ? `&overlay=${overlay}` : ""); - - let fileName = - record.tonieInfo?.series || record.tonieInfo?.episode - ? `${record.tonieInfo.series || ""}${record.tonieInfo.episode ? " - " + record.tonieInfo.episode : ""}` - : record.name; - - if (!record.tonieInfo?.series && !record.tonieInfo?.episode && fileName.endsWith(".taf")) { - fileName = fileName.replace(/\.taf$/i, ".ogg"); - } + (query ? `?${query}` : ""); + + const isTaf = record.name.toLowerCase().endsWith(".taf"); + + const tonieName = [record.tonieInfo?.series, record.tonieInfo?.episode] + .filter(Boolean) + .join(" - "); + + const fileName = isTaf + ? `${tonieName || record.name.replace(/\.taf$/i, "")}.ogg` + : record.name; setDownloading((prev) => ({ ...prev, [record.name]: true })); From 6b6595cf77abe09023f316625983c8e8d851727d Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:54:23 +0200 Subject: [PATCH 21/25] fix: ogg and selected track download in content section works now, no 502 error anymore. --- .../tonies/filebrowser/FileBrowser.tsx | 62 +++++++++++++++++-- .../filebrowser/hooks/useFileBrowserCore.tsx | 45 +++++++++----- 2 files changed, 87 insertions(+), 20 deletions(-) diff --git a/src/components/tonies/filebrowser/FileBrowser.tsx b/src/components/tonies/filebrowser/FileBrowser.tsx index 6b43e6bc..ebd0a711 100644 --- a/src/components/tonies/filebrowser/FileBrowser.tsx +++ b/src/components/tonies/filebrowser/FileBrowser.tsx @@ -57,9 +57,13 @@ import { CustomModelEditor } from "../custommodel/CustomModelEditor"; import { UnusedTAFsModal } from "./modals/UnusedTAFsModal"; import CustomJsonSnippetModal from "./modals/CustomJsonSnippetModal"; import { backendUrl, withBase } from "../../../utils/basePath"; +import { TeddyCloudApi } from "../../../api"; +import { defaultAPIConfig } from "../../../config/defaultApiConfig"; const { Paragraph } = Typography; +const api = new TeddyCloudApi(defaultAPIConfig()); + const { useToken } = theme; export const FileBrowser: React.FC<{ @@ -208,12 +212,60 @@ export const FileBrowser: React.FC<{ }); // information modal - const showInformationModal = (record: any) => { - if (!record.isDir && record.tonieInfo?.tracks) { - setCurrentRecord(record); - setCurrentAudioUrl(buildContentUrl(record.name, { ogg: true })); - setIsInformationModalOpen(true); + const buildLibraryTafContentUrl = (source: unknown): string | null => { + if (typeof source !== "string" || !source.toLowerCase().startsWith("lib://")) { + return null; + } + + let libraryPath = source.slice("lib://".length).replace(/^\/+/, ""); + try { + libraryPath = decodeURIComponent(libraryPath); + } catch { + // Keep the original path if it is not URI encoded correctly. + } + + libraryPath = libraryPath.replace(/\\/g, "/").replace(/\/{2,}/g, "/"); + if (!libraryPath.toLowerCase().endsWith(".taf")) { + return null; + } + + const pathParts = libraryPath.split("/").filter(Boolean); + const fileName = pathParts.pop(); + if (!fileName) { + return null; + } + + return buildContentUrl(fileName, { + ogg: true, + path: pathParts.join("/"), + special: "library", + }); + }; + + const showInformationModal = async (record: Record) => { + if (record.isDir || !record.tonieInfo?.tracks) { + return; } + + let audioUrl = buildContentUrl(record.name, { ogg: true }); + + if (record.name.toLowerCase().match(/500304e0\.json$/)) { + try { + const response = await api.apiGetTeddyCloudApiRaw(buildContentUrl(record.name)); + const contentJson = await response.json(); + const sourceAudioUrl = buildLibraryTafContentUrl(contentJson?.source); + + if (sourceAudioUrl) { + audioUrl = sourceAudioUrl; + } + } catch (error) { + console.warn("Could not resolve audio source from content JSON", error); + } + } + + setCurrentRecord(record); + setCurrentAudioUrl(audioUrl); + setIsInformationModalOpen(true); }; // json viewer diff --git a/src/components/tonies/filebrowser/hooks/useFileBrowserCore.tsx b/src/components/tonies/filebrowser/hooks/useFileBrowserCore.tsx index 103f88c9..316f3d68 100644 --- a/src/components/tonies/filebrowser/hooks/useFileBrowserCore.tsx +++ b/src/components/tonies/filebrowser/hooks/useFileBrowserCore.tsx @@ -49,7 +49,10 @@ interface UseFileBrowserCoreResult { generateBreadcrumbs: (currentPath: string) => React.ReactNode; buildDirPath: (dirPath: string) => string; - buildContentUrl: (fileName: string, options?: { ogg?: boolean }) => string; + buildContentUrl: ( + fileName: string, + options?: { ogg?: boolean; path?: string; special?: string }, + ) => string; defaultSorter: (a: any, b: any, dataIndex: string | string[]) => number; dirNameSorter: (a: any, b: any) => number; @@ -421,22 +424,31 @@ export const useFileBrowserCore = ({ return path ? `${path}/${dirPath}` : dirPath; }; - const buildContentUrl = (fileName: string, options?: { ogg?: boolean }): string => { - const { ogg = false } = options || {}; + const buildContentUrl = ( + fileName: string, + options?: { ogg?: boolean; path?: string; special?: string }, + ): string => { + const { ogg = false, path: pathOverride, special: specialOverride } = options || {}; const encodedPath = - mode === "fileBrowser" - ? path || "" - : path - ? path - .split("/") - .map((segment) => encodeURIComponent(segment)) - .join("/") - : ""; - + pathOverride !== undefined + ? pathOverride + .split("/") + .filter(Boolean) + .map((segment) => encodeURIComponent(segment)) + .join("/") + : mode === "fileBrowser" + ? path || "" + : path + ? path + .split("/") + .map((segment) => encodeURIComponent(segment)) + .join("/") + : ""; + const targetSpecial = specialOverride !== undefined ? specialOverride : special; const encodedName = encodeURIComponent(fileName); - if (special === "custom_img") { + if (targetSpecial === "custom_img") { return encodedPath ? `/custom_img/${encodedPath}/${encodedName}` : `/custom_img/${encodedName}`; @@ -446,10 +458,13 @@ export const useFileBrowserCore = ({ const params = new URLSearchParams(); if (ogg) params.set("ogg", "true"); - params.set("special", special); + if (targetSpecial) params.set("special", targetSpecial); if (overlay) params.set("overlay", overlay); - url += `?${params.toString()}`; + const query = params.toString(); + if (query) { + url += `?${query}`; + } return url; }; From c82c70ded2c9a6f33c53d9531d02bb8b15b8d0f2 Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:10:21 +0200 Subject: [PATCH 22/25] fix: Download trackselection on tonies overview works now fine --- .../common/modals/TonieInformationModal.tsx | 51 +++++++++++++++++-- src/utils/downloads/tafDownload.ts | 7 ++- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/components/tonies/common/modals/TonieInformationModal.tsx b/src/components/tonies/common/modals/TonieInformationModal.tsx index 05dcb2b6..02cab184 100644 --- a/src/components/tonies/common/modals/TonieInformationModal.tsx +++ b/src/components/tonies/common/modals/TonieInformationModal.tsx @@ -140,13 +140,56 @@ const TonieInformationModal: React.FC = ({ displayTrackSeconds, displayLengthSeconds, ); + const librarySourceContentUrl = (() => { + if (!("source" in tonieCardOrTAFRecord)) { + return null; + } + + const source = tonieCardOrTAFRecord.source?.trim(); + if (!source?.toLowerCase().startsWith("lib://")) { + return null; + } + + let libraryPath = source.slice("lib://".length).replace(/^\/+/, ""); + try { + libraryPath = decodeURIComponent(libraryPath); + } catch { + // Keep the original path if it is not URI encoded correctly. + } + + libraryPath = libraryPath.replace(/\\/g, "/").replace(/\/{2,}/g, "/"); + if (!libraryPath.toLowerCase().endsWith(".taf")) { + return null; + } + + const pathParts = libraryPath.split("/").filter(Boolean); + const fileName = pathParts.pop(); + if (!fileName) { + return null; + } + + const encodedPath = pathParts.map((part) => encodeURIComponent(part)).join("/"); + const params = new URLSearchParams(); + params.set("ogg", "true"); + params.set("special", "library"); + if (overlay) { + params.set("overlay", overlay); + } + + return withBase( + `/content/${encodedPath ? `${encodedPath}/` : ""}${encodeURIComponent(fileName)}?${params.toString()}`, + ); + })(); + const canDownloadAudio = - "audioUrl" in tonieCardOrTAFRecord && - (!("exists" in tonieCardOrTAFRecord) || tonieCardOrTAFRecord.exists); + librarySourceContentUrl !== null || + ("audioUrl" in tonieCardOrTAFRecord && + (!("exists" in tonieCardOrTAFRecord) || tonieCardOrTAFRecord.exists)); const downloadContentUrl = - canDownloadAudio && "audioUrl" in tonieCardOrTAFRecord + librarySourceContentUrl ?? + ("audioUrl" in tonieCardOrTAFRecord ? withBase(toSameOriginUrl(tonieCardOrTAFRecord.audioUrl)) - : ""; + : ""); const trackSecondsMatchSourceTracks = ( tonieCardOrTAFRecord: TonieCardTAFRecord, diff --git a/src/utils/downloads/tafDownload.ts b/src/utils/downloads/tafDownload.ts index 55351a98..fc53ce7b 100644 --- a/src/utils/downloads/tafDownload.ts +++ b/src/utils/downloads/tafDownload.ts @@ -53,9 +53,12 @@ export function buildTafDownloadUrl( parsed.searchParams.delete("filename"); parsed.searchParams.delete("entry"); - // Track titles stay out of the query. The server only accepts 255 characters, - // and a multi-track name list exceeds that and is dropped as an empty reply. + // /content/download/... serves the already prepared audio stream and does not + // support exporting individual TAF tracks. For a track selection, address the + // underlying cached TAF through the regular /content/... route instead. if (options.tracks && options.tracks.length > 0) { + parsed.pathname = parsed.pathname.replace("/content/download/", "/content/"); + parsed.searchParams.delete("skip_header"); parsed.searchParams.set("tracks", options.tracks.join(",")); } From ec0200d8a6855555a0652a4456b14f9e98490218 Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:19:32 +0200 Subject: [PATCH 23/25] fix: update dependency for selected keys reset to use selection scope --- src/components/tonies/common/modals/TafTrackDownloadModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/tonies/common/modals/TafTrackDownloadModal.tsx b/src/components/tonies/common/modals/TafTrackDownloadModal.tsx index f1d07cee..b5d7a649 100644 --- a/src/components/tonies/common/modals/TafTrackDownloadModal.tsx +++ b/src/components/tonies/common/modals/TafTrackDownloadModal.tsx @@ -73,13 +73,14 @@ export const TafDownloadPanel: React.FC = ({ const { token } = useToken(); const canSplit = tracks.length > 1; const allKeys = useMemo(() => tracks.map((track) => track.number), [tracks]); + const selectionScope = `${contentUrl}|${allKeys.join(",")}`; const [selectedKeys, setSelectedKeys] = useState([]); const [downloadingWholeFile, setDownloadingWholeFile] = useState(false); const [downloadingSelected, setDownloadingSelected] = useState(false); useEffect(() => { setSelectedKeys([]); - }, [allKeys]); + }, [selectionScope]); const downloadBase = sanitizeDownloadName(baseFilename || "download"); From 159626ebc65decfdede9e322f493bb57e1706144 Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:34:33 +0200 Subject: [PATCH 24/25] chore: updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cfff02e..1b2b7ed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ - gui: Track names in the Custom Model Editor keep their position, unnamed tracks in between are no longer dropped - gui: Box setup can create a port patch for CC3200 boxes (altPort.custom.json) and patch the port of ESP32 boxes, e.g. for a proxy that routes boxes by port (not possible for CC3235 boxes and the Toniebox 2) - gui: Show a loading spinner and prevent repeated clicks while a TonieCard background download is in progress -- gui: fix filedownload in content. 500304E0.json files were downloaded as invalid ogg files. +- gui: Fixed TAF track downloads in the TonieInformationModal to use the correct linked/source TAF for Content and Tonies views, including uncached Tonies, and keep track selections across re-renders, screen size changes and audio player updates ### Commits From b9c33bbfeb2cea093899bd78aec6c354cf561836 Mon Sep 17 00:00:00 2001 From: henryk86 <166761451+henryk86@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:03:42 +0200 Subject: [PATCH 25/25] reverts c82c70ded2c9a6f33c53d9531d02bb8b15b8d0f2 --- .../common/modals/TonieInformationModal.tsx | 50 ++----------------- src/utils/downloads/tafDownload.ts | 7 +-- 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/src/components/tonies/common/modals/TonieInformationModal.tsx b/src/components/tonies/common/modals/TonieInformationModal.tsx index 02cab184..19e12427 100644 --- a/src/components/tonies/common/modals/TonieInformationModal.tsx +++ b/src/components/tonies/common/modals/TonieInformationModal.tsx @@ -140,56 +140,14 @@ const TonieInformationModal: React.FC = ({ displayTrackSeconds, displayLengthSeconds, ); - const librarySourceContentUrl = (() => { - if (!("source" in tonieCardOrTAFRecord)) { - return null; - } - - const source = tonieCardOrTAFRecord.source?.trim(); - if (!source?.toLowerCase().startsWith("lib://")) { - return null; - } - - let libraryPath = source.slice("lib://".length).replace(/^\/+/, ""); - try { - libraryPath = decodeURIComponent(libraryPath); - } catch { - // Keep the original path if it is not URI encoded correctly. - } - - libraryPath = libraryPath.replace(/\\/g, "/").replace(/\/{2,}/g, "/"); - if (!libraryPath.toLowerCase().endsWith(".taf")) { - return null; - } - - const pathParts = libraryPath.split("/").filter(Boolean); - const fileName = pathParts.pop(); - if (!fileName) { - return null; - } - - const encodedPath = pathParts.map((part) => encodeURIComponent(part)).join("/"); - const params = new URLSearchParams(); - params.set("ogg", "true"); - params.set("special", "library"); - if (overlay) { - params.set("overlay", overlay); - } - - return withBase( - `/content/${encodedPath ? `${encodedPath}/` : ""}${encodeURIComponent(fileName)}?${params.toString()}`, - ); - })(); const canDownloadAudio = - librarySourceContentUrl !== null || - ("audioUrl" in tonieCardOrTAFRecord && - (!("exists" in tonieCardOrTAFRecord) || tonieCardOrTAFRecord.exists)); + "audioUrl" in tonieCardOrTAFRecord && + (!("exists" in tonieCardOrTAFRecord) || tonieCardOrTAFRecord.exists); const downloadContentUrl = - librarySourceContentUrl ?? - ("audioUrl" in tonieCardOrTAFRecord + canDownloadAudio && "audioUrl" in tonieCardOrTAFRecord ? withBase(toSameOriginUrl(tonieCardOrTAFRecord.audioUrl)) - : ""); + : ""; const trackSecondsMatchSourceTracks = ( tonieCardOrTAFRecord: TonieCardTAFRecord, diff --git a/src/utils/downloads/tafDownload.ts b/src/utils/downloads/tafDownload.ts index fc53ce7b..55351a98 100644 --- a/src/utils/downloads/tafDownload.ts +++ b/src/utils/downloads/tafDownload.ts @@ -53,12 +53,9 @@ export function buildTafDownloadUrl( parsed.searchParams.delete("filename"); parsed.searchParams.delete("entry"); - // /content/download/... serves the already prepared audio stream and does not - // support exporting individual TAF tracks. For a track selection, address the - // underlying cached TAF through the regular /content/... route instead. + // Track titles stay out of the query. The server only accepts 255 characters, + // and a multi-track name list exceeds that and is dropped as an empty reply. if (options.tracks && options.tracks.length > 0) { - parsed.pathname = parsed.pathname.replace("/content/download/", "/content/"); - parsed.searchParams.delete("skip_header"); parsed.searchParams.set("tracks", options.tracks.join(",")); }