diff --git a/frontend/js/i18n/de.js b/frontend/js/i18n/de.js index d401a30..8e0435b 100644 --- a/frontend/js/i18n/de.js +++ b/frontend/js/i18n/de.js @@ -625,4 +625,66 @@ export default { 'designer.toast.export_failed': 'Export fehlgeschlagen: {error}', 'designer.toast.loaded': 'Design geladen', 'designer.toast.invalid_file': 'Ungültige Design-Datei', + + // Playlists + 'playlist.title': 'Playlists', + 'playlist.subtitle': 'Erstellen und verwalten Sie Inhaltslisten', + 'playlist.show_auto_generated': 'Auto-generierte anzeigen', + 'playlist.new_playlist_btn': '+ Neue Playlist', + 'playlist.new_playlist': 'Neue Playlist', + 'playlist.empty_title': 'Noch keine Playlists', + 'playlist.empty_desc': 'Erstellen Sie Ihre erste Playlist, um Inhalte für Ihre Bildschirme zu organisieren.', + 'playlist.all_auto_generated': 'Alle Playlists sind auto-generiert. Aktivieren Sie „Auto-generierte anzeigen", um sie zu sehen.', + 'playlist.tag_auto': 'auto', + 'playlist.tag_draft': 'Entwurf', + 'playlist.item_count_one': '1 Element', + 'playlist.item_count_other': '{n} Elemente', + 'playlist.created_at': 'Erstellt {date}', + 'playlist.display_count_one': '1 Bildschirm', + 'playlist.display_count_other': '{n} Bildschirme', + 'playlist.assigned_to_one': 'Zugewiesen an 1 Bildschirm', + 'playlist.assigned_to_other': 'Zugewiesen an {n} Bildschirme', + 'playlist.load_failed': 'Playlists konnten nicht geladen werden: {error}', + 'playlist.back_to_playlists': 'Zurück zu Playlists', + 'playlist.name_placeholder': 'Playlist-Name', + 'playlist.desc_placeholder': 'Beschreibung (optional)', + 'playlist.create_btn': 'Erstellen', + 'playlist.add_desc_placeholder': 'Beschreibung hinzufügen...', + 'playlist.click_to_rename': 'Klicken zum Umbenennen', + 'playlist.click_to_edit_desc': 'Klicken zum Bearbeiten der Beschreibung', + 'playlist.add_content': '+ Inhalt hinzufügen', + 'playlist.delete_playlist': 'Playlist löschen', + 'playlist.back': 'Zurück', + 'playlist.items_empty': 'Diese Playlist ist leer', + 'playlist.items_empty_hint': 'Klicken Sie auf „Inhalt hinzufügen", um Elemente hinzuzufügen.', + 'playlist.duration': 'Dauer', + 'playlist.sec': 'Sek', + 'playlist.move_up': 'Nach oben', + 'playlist.move_down': 'Nach unten', + 'playlist.remove_item': 'Element entfernen', + 'playlist.item_widget': 'Widget', + 'playlist.unknown_type': 'Unbekannter Typ', + 'playlist.confirm_delete': '„{name}" löschen? Dies kann nicht rückgängig gemacht werden.', + 'playlist.confirm_discard_draft': 'Alle nicht veröffentlichten Änderungen verwerfen und zur letzten veröffentlichten Version zurückkehren?', + 'playlist.draft.banner_title': 'Unveröffentlichte Änderungen', + 'playlist.draft.devices_showing_published': 'Geräte zeigen weiterhin die zuletzt veröffentlichte Version.', + 'playlist.draft.never_published': 'Diese Playlist wurde noch nie veröffentlicht. Geräte zeigen nichts an, bis Sie veröffentlichen.', + 'playlist.draft.discard_changes': 'Änderungen verwerfen', + 'playlist.draft.publish': 'Veröffentlichen', + 'playlist.draft.publishing': 'Wird veröffentlicht...', + 'playlist.toast.created': 'Playlist erstellt', + 'playlist.toast.deleted': 'Playlist gelöscht', + 'playlist.toast.published': 'Playlist veröffentlicht — Geräte aktualisiert', + 'playlist.toast.draft_discarded': 'Entwurfsänderungen verworfen', + 'playlist.toast.item_removed': 'Element entfernt', + 'playlist.add_modal_title': 'Inhalt zur Playlist hinzufügen', + 'playlist.tab_content': 'Inhalt', + 'playlist.tab_widgets': 'Widgets', + 'playlist.search_placeholder': 'Suchen...', + 'playlist.close': 'Schließen', + 'playlist.no_content_found': 'Kein Inhalt gefunden', + 'playlist.no_widgets_found': 'Keine Widgets gefunden', + 'playlist.add_btn': 'Hinzufügen', + 'playlist.adding': 'Wird hinzugefügt...', + 'playlist.added': 'Hinzugefügt', }; diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 1839253..086c260 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -661,4 +661,66 @@ export default { 'designer.toast.export_failed': 'Export failed: {error}', 'designer.toast.loaded': 'Design loaded', 'designer.toast.invalid_file': 'Invalid design file', + + // Playlists + 'playlist.title': 'Playlists', + 'playlist.subtitle': 'Create and manage content playlists', + 'playlist.show_auto_generated': 'Show auto-generated', + 'playlist.new_playlist_btn': '+ New Playlist', + 'playlist.new_playlist': 'New Playlist', + 'playlist.empty_title': 'No playlists yet', + 'playlist.empty_desc': 'Create your first playlist to organize content for your displays.', + 'playlist.all_auto_generated': 'All playlists are auto-generated. Toggle "Show auto-generated" to see them.', + 'playlist.tag_auto': 'auto', + 'playlist.tag_draft': 'draft', + 'playlist.item_count_one': '1 item', + 'playlist.item_count_other': '{n} items', + 'playlist.created_at': 'Created {date}', + 'playlist.display_count_one': '1 display', + 'playlist.display_count_other': '{n} displays', + 'playlist.assigned_to_one': 'Assigned to 1 display', + 'playlist.assigned_to_other': 'Assigned to {n} displays', + 'playlist.load_failed': 'Failed to load playlists: {error}', + 'playlist.back_to_playlists': 'Back to Playlists', + 'playlist.name_placeholder': 'Playlist name', + 'playlist.desc_placeholder': 'Description (optional)', + 'playlist.create_btn': 'Create', + 'playlist.add_desc_placeholder': 'Add a description...', + 'playlist.click_to_rename': 'Click to rename', + 'playlist.click_to_edit_desc': 'Click to edit description', + 'playlist.add_content': '+ Add Content', + 'playlist.delete_playlist': 'Delete Playlist', + 'playlist.back': 'Back', + 'playlist.items_empty': 'This playlist is empty', + 'playlist.items_empty_hint': 'Click "Add Content" to add items.', + 'playlist.duration': 'Duration', + 'playlist.sec': 'sec', + 'playlist.move_up': 'Move up', + 'playlist.move_down': 'Move down', + 'playlist.remove_item': 'Remove item', + 'playlist.item_widget': 'Widget', + 'playlist.unknown_type': 'Unknown type', + 'playlist.confirm_delete': 'Delete "{name}"? This cannot be undone.', + 'playlist.confirm_discard_draft': 'Discard all unpublished changes and revert to the last published version?', + 'playlist.draft.banner_title': 'Unpublished changes', + 'playlist.draft.devices_showing_published': 'Devices are still showing the last published version.', + 'playlist.draft.never_published': 'This playlist has never been published. Devices will show nothing until you publish.', + 'playlist.draft.discard_changes': 'Discard Changes', + 'playlist.draft.publish': 'Publish', + 'playlist.draft.publishing': 'Publishing...', + 'playlist.toast.created': 'Playlist created', + 'playlist.toast.deleted': 'Playlist deleted', + 'playlist.toast.published': 'Playlist published — devices updated', + 'playlist.toast.draft_discarded': 'Draft changes discarded', + 'playlist.toast.item_removed': 'Item removed', + 'playlist.add_modal_title': 'Add Content to Playlist', + 'playlist.tab_content': 'Content', + 'playlist.tab_widgets': 'Widgets', + 'playlist.search_placeholder': 'Search...', + 'playlist.close': 'Close', + 'playlist.no_content_found': 'No content found', + 'playlist.no_widgets_found': 'No widgets found', + 'playlist.add_btn': 'Add', + 'playlist.adding': 'Adding...', + 'playlist.added': 'Added', }; diff --git a/frontend/js/i18n/es.js b/frontend/js/i18n/es.js index f085d54..dd98800 100644 --- a/frontend/js/i18n/es.js +++ b/frontend/js/i18n/es.js @@ -624,4 +624,66 @@ export default { 'designer.toast.export_failed': 'Falló la exportación: {error}', 'designer.toast.loaded': 'Diseño cargado', 'designer.toast.invalid_file': 'Archivo de diseño no válido', + + // Playlists + 'playlist.title': 'Listas de reproducción', + 'playlist.subtitle': 'Crea y gestiona listas de contenido', + 'playlist.show_auto_generated': 'Mostrar autogeneradas', + 'playlist.new_playlist_btn': '+ Nueva lista', + 'playlist.new_playlist': 'Nueva lista', + 'playlist.empty_title': 'Aún no hay listas', + 'playlist.empty_desc': 'Crea tu primera lista para organizar contenido para tus pantallas.', + 'playlist.all_auto_generated': 'Todas las listas son autogeneradas. Activa "Mostrar autogeneradas" para verlas.', + 'playlist.tag_auto': 'auto', + 'playlist.tag_draft': 'borrador', + 'playlist.item_count_one': '1 elemento', + 'playlist.item_count_other': '{n} elementos', + 'playlist.created_at': 'Creado {date}', + 'playlist.display_count_one': '1 pantalla', + 'playlist.display_count_other': '{n} pantallas', + 'playlist.assigned_to_one': 'Asignada a 1 pantalla', + 'playlist.assigned_to_other': 'Asignada a {n} pantallas', + 'playlist.load_failed': 'Error al cargar las listas: {error}', + 'playlist.back_to_playlists': 'Volver a listas', + 'playlist.name_placeholder': 'Nombre de la lista', + 'playlist.desc_placeholder': 'Descripción (opcional)', + 'playlist.create_btn': 'Crear', + 'playlist.add_desc_placeholder': 'Agregar una descripción...', + 'playlist.click_to_rename': 'Clic para renombrar', + 'playlist.click_to_edit_desc': 'Clic para editar descripción', + 'playlist.add_content': '+ Agregar contenido', + 'playlist.delete_playlist': 'Eliminar lista', + 'playlist.back': 'Atrás', + 'playlist.items_empty': 'Esta lista está vacía', + 'playlist.items_empty_hint': 'Haz clic en "Agregar contenido" para añadir elementos.', + 'playlist.duration': 'Duración', + 'playlist.sec': 'seg', + 'playlist.move_up': 'Subir', + 'playlist.move_down': 'Bajar', + 'playlist.remove_item': 'Quitar elemento', + 'playlist.item_widget': 'Widget', + 'playlist.unknown_type': 'Tipo desconocido', + 'playlist.confirm_delete': '¿Eliminar "{name}"? Esto no se puede deshacer.', + 'playlist.confirm_discard_draft': '¿Descartar todos los cambios no publicados y volver a la última versión publicada?', + 'playlist.draft.banner_title': 'Cambios sin publicar', + 'playlist.draft.devices_showing_published': 'Los dispositivos siguen mostrando la última versión publicada.', + 'playlist.draft.never_published': 'Esta lista nunca se ha publicado. Los dispositivos no mostrarán nada hasta que la publiques.', + 'playlist.draft.discard_changes': 'Descartar cambios', + 'playlist.draft.publish': 'Publicar', + 'playlist.draft.publishing': 'Publicando...', + 'playlist.toast.created': 'Lista creada', + 'playlist.toast.deleted': 'Lista eliminada', + 'playlist.toast.published': 'Lista publicada — dispositivos actualizados', + 'playlist.toast.draft_discarded': 'Cambios del borrador descartados', + 'playlist.toast.item_removed': 'Elemento eliminado', + 'playlist.add_modal_title': 'Agregar contenido a la lista', + 'playlist.tab_content': 'Contenido', + 'playlist.tab_widgets': 'Widgets', + 'playlist.search_placeholder': 'Buscar...', + 'playlist.close': 'Cerrar', + 'playlist.no_content_found': 'No se encontró contenido', + 'playlist.no_widgets_found': 'No se encontraron widgets', + 'playlist.add_btn': 'Agregar', + 'playlist.adding': 'Agregando...', + 'playlist.added': 'Agregado', }; diff --git a/frontend/js/i18n/fr.js b/frontend/js/i18n/fr.js index 38d039b..c0175ab 100644 --- a/frontend/js/i18n/fr.js +++ b/frontend/js/i18n/fr.js @@ -625,4 +625,66 @@ export default { 'designer.toast.export_failed': 'Échec de l\'export : {error}', 'designer.toast.loaded': 'Design chargé', 'designer.toast.invalid_file': 'Fichier de design invalide', + + // Playlists + 'playlist.title': 'Listes de lecture', + 'playlist.subtitle': 'Créez et gérez des listes de contenu', + 'playlist.show_auto_generated': 'Afficher les listes auto', + 'playlist.new_playlist_btn': '+ Nouvelle liste', + 'playlist.new_playlist': 'Nouvelle liste', + 'playlist.empty_title': 'Pas encore de listes', + 'playlist.empty_desc': 'Créez votre première liste pour organiser le contenu de vos écrans.', + 'playlist.all_auto_generated': 'Toutes les listes sont auto-générées. Cochez « Afficher les listes auto » pour les voir.', + 'playlist.tag_auto': 'auto', + 'playlist.tag_draft': 'brouillon', + 'playlist.item_count_one': '1 élément', + 'playlist.item_count_other': '{n} éléments', + 'playlist.created_at': 'Créée {date}', + 'playlist.display_count_one': '1 écran', + 'playlist.display_count_other': '{n} écrans', + 'playlist.assigned_to_one': 'Attribuée à 1 écran', + 'playlist.assigned_to_other': 'Attribuée à {n} écrans', + 'playlist.load_failed': 'Échec du chargement des listes : {error}', + 'playlist.back_to_playlists': 'Retour aux listes', + 'playlist.name_placeholder': 'Nom de la liste', + 'playlist.desc_placeholder': 'Description (facultatif)', + 'playlist.create_btn': 'Créer', + 'playlist.add_desc_placeholder': 'Ajouter une description...', + 'playlist.click_to_rename': 'Cliquer pour renommer', + 'playlist.click_to_edit_desc': 'Cliquer pour modifier la description', + 'playlist.add_content': '+ Ajouter du contenu', + 'playlist.delete_playlist': 'Supprimer la liste', + 'playlist.back': 'Retour', + 'playlist.items_empty': 'Cette liste est vide', + 'playlist.items_empty_hint': 'Cliquez sur « Ajouter du contenu » pour ajouter des éléments.', + 'playlist.duration': 'Durée', + 'playlist.sec': 'sec', + 'playlist.move_up': 'Monter', + 'playlist.move_down': 'Descendre', + 'playlist.remove_item': 'Retirer l\'élément', + 'playlist.item_widget': 'Widget', + 'playlist.unknown_type': 'Type inconnu', + 'playlist.confirm_delete': 'Supprimer « {name} » ? Cette action est irréversible.', + 'playlist.confirm_discard_draft': 'Annuler toutes les modifications non publiées et revenir à la dernière version publiée ?', + 'playlist.draft.banner_title': 'Modifications non publiées', + 'playlist.draft.devices_showing_published': 'Les appareils affichent encore la dernière version publiée.', + 'playlist.draft.never_published': 'Cette liste n\'a jamais été publiée. Les appareils n\'afficheront rien jusqu\'à publication.', + 'playlist.draft.discard_changes': 'Annuler les modifications', + 'playlist.draft.publish': 'Publier', + 'playlist.draft.publishing': 'Publication...', + 'playlist.toast.created': 'Liste créée', + 'playlist.toast.deleted': 'Liste supprimée', + 'playlist.toast.published': 'Liste publiée — appareils mis à jour', + 'playlist.toast.draft_discarded': 'Modifications du brouillon annulées', + 'playlist.toast.item_removed': 'Élément retiré', + 'playlist.add_modal_title': 'Ajouter du contenu à la liste', + 'playlist.tab_content': 'Contenu', + 'playlist.tab_widgets': 'Widgets', + 'playlist.search_placeholder': 'Rechercher...', + 'playlist.close': 'Fermer', + 'playlist.no_content_found': 'Aucun contenu trouvé', + 'playlist.no_widgets_found': 'Aucun widget trouvé', + 'playlist.add_btn': 'Ajouter', + 'playlist.adding': 'Ajout...', + 'playlist.added': 'Ajouté', }; diff --git a/frontend/js/i18n/pt.js b/frontend/js/i18n/pt.js index 8171d55..b80cbd6 100644 --- a/frontend/js/i18n/pt.js +++ b/frontend/js/i18n/pt.js @@ -625,4 +625,66 @@ export default { 'designer.toast.export_failed': 'Falha ao exportar: {error}', 'designer.toast.loaded': 'Design carregado', 'designer.toast.invalid_file': 'Arquivo de design inválido', + + // Playlists + 'playlist.title': 'Playlists', + 'playlist.subtitle': 'Crie e gerencie playlists de conteúdo', + 'playlist.show_auto_generated': 'Mostrar autogeradas', + 'playlist.new_playlist_btn': '+ Nova playlist', + 'playlist.new_playlist': 'Nova playlist', + 'playlist.empty_title': 'Sem playlists ainda', + 'playlist.empty_desc': 'Crie sua primeira playlist para organizar conteúdo para suas telas.', + 'playlist.all_auto_generated': 'Todas as playlists são autogeradas. Ative "Mostrar autogeradas" para vê-las.', + 'playlist.tag_auto': 'auto', + 'playlist.tag_draft': 'rascunho', + 'playlist.item_count_one': '1 item', + 'playlist.item_count_other': '{n} itens', + 'playlist.created_at': 'Criada {date}', + 'playlist.display_count_one': '1 tela', + 'playlist.display_count_other': '{n} telas', + 'playlist.assigned_to_one': 'Atribuída a 1 tela', + 'playlist.assigned_to_other': 'Atribuída a {n} telas', + 'playlist.load_failed': 'Falha ao carregar playlists: {error}', + 'playlist.back_to_playlists': 'Voltar para playlists', + 'playlist.name_placeholder': 'Nome da playlist', + 'playlist.desc_placeholder': 'Descrição (opcional)', + 'playlist.create_btn': 'Criar', + 'playlist.add_desc_placeholder': 'Adicionar uma descrição...', + 'playlist.click_to_rename': 'Clique para renomear', + 'playlist.click_to_edit_desc': 'Clique para editar a descrição', + 'playlist.add_content': '+ Adicionar conteúdo', + 'playlist.delete_playlist': 'Excluir playlist', + 'playlist.back': 'Voltar', + 'playlist.items_empty': 'Esta playlist está vazia', + 'playlist.items_empty_hint': 'Clique em "Adicionar conteúdo" para adicionar itens.', + 'playlist.duration': 'Duração', + 'playlist.sec': 'seg', + 'playlist.move_up': 'Mover para cima', + 'playlist.move_down': 'Mover para baixo', + 'playlist.remove_item': 'Remover item', + 'playlist.item_widget': 'Widget', + 'playlist.unknown_type': 'Tipo desconhecido', + 'playlist.confirm_delete': 'Excluir "{name}"? Isso não pode ser desfeito.', + 'playlist.confirm_discard_draft': 'Descartar todas as alterações não publicadas e voltar à última versão publicada?', + 'playlist.draft.banner_title': 'Alterações não publicadas', + 'playlist.draft.devices_showing_published': 'Os dispositivos ainda exibem a última versão publicada.', + 'playlist.draft.never_published': 'Esta playlist nunca foi publicada. Os dispositivos não exibirão nada até você publicar.', + 'playlist.draft.discard_changes': 'Descartar alterações', + 'playlist.draft.publish': 'Publicar', + 'playlist.draft.publishing': 'Publicando...', + 'playlist.toast.created': 'Playlist criada', + 'playlist.toast.deleted': 'Playlist excluída', + 'playlist.toast.published': 'Playlist publicada — dispositivos atualizados', + 'playlist.toast.draft_discarded': 'Alterações do rascunho descartadas', + 'playlist.toast.item_removed': 'Item removido', + 'playlist.add_modal_title': 'Adicionar conteúdo à playlist', + 'playlist.tab_content': 'Conteúdo', + 'playlist.tab_widgets': 'Widgets', + 'playlist.search_placeholder': 'Buscar...', + 'playlist.close': 'Fechar', + 'playlist.no_content_found': 'Nenhum conteúdo encontrado', + 'playlist.no_widgets_found': 'Nenhum widget encontrado', + 'playlist.add_btn': 'Adicionar', + 'playlist.adding': 'Adicionando...', + 'playlist.added': 'Adicionado', }; diff --git a/frontend/js/views/playlists.js b/frontend/js/views/playlists.js index e7fb222..edbc0e5 100644 --- a/frontend/js/views/playlists.js +++ b/frontend/js/views/playlists.js @@ -1,6 +1,7 @@ import { api } from '../api.js'; import { showToast } from '../components/toast.js'; import { esc } from '../utils.js'; +import { t, tn } from '../i18n.js'; function formatDate(ts) { if (!ts) return '--'; @@ -31,27 +32,25 @@ export function cleanup() { currentPlaylistId = null; } -// ==================== LIST VIEW ==================== - let showAutoGenerated = true; async function renderList(container) { container.innerHTML = `
-
Loading...
+
${t('common.loading')}
`; @@ -76,8 +75,8 @@ async function loadPlaylists() { -

No playlists yet

-

Create your first playlist to organize content for your displays.

+

${t('playlist.empty_title')}

+

${t('playlist.empty_desc')}

`; return; @@ -87,7 +86,7 @@ async function loadPlaylists() { if (!filtered.length) { grid.innerHTML = `
- ${playlists.length ? 'All playlists are auto-generated. Toggle "Show auto-generated" to see them.' : ''} + ${playlists.length ? t('playlist.all_auto_generated') : ''}
`; return; @@ -98,20 +97,20 @@ async function loadPlaylists() {
${esc(p.name)}
- ${p.is_auto_generated ? 'auto' : ''} - ${p.status === 'draft' ? 'draft' : ''} + ${p.is_auto_generated ? `${t('playlist.tag_auto')}` : ''} + ${p.status === 'draft' ? `${t('playlist.tag_draft')}` : ''}
-
${p.item_count} item${p.item_count !== 1 ? 's' : ''}
+
${tn('playlist.item_count', p.item_count)}
${p.description ? `
${esc(p.description)}
` : ''}
- Created ${formatDate(p.created_at)} - ${p.display_count ? `${p.display_count} display${p.display_count !== 1 ? 's' : ''}` : ''} + ${t('playlist.created_at', { date: formatDate(p.created_at) })} + ${p.display_count ? `${tn('playlist.display_count', p.display_count)}` : ''}
`).join(''); } catch (err) { - grid.innerHTML = `
Failed to load playlists: ${esc(err.message)}
`; + grid.innerHTML = `
${t('playlist.load_failed', { error: esc(err.message) })}
`; } } @@ -120,12 +119,12 @@ function showCreateModal() { modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000'; modal.innerHTML = `
-

New Playlist

- - +

${t('playlist.new_playlist')}

+ +
- - + +
`; @@ -144,7 +143,7 @@ function showCreateModal() { try { const pl = await api.createPlaylist(name, desc); modal.remove(); - showToast('Playlist created'); + showToast(t('playlist.toast.created')); window.location.hash = `#/playlists/${pl.id}`; } catch (err) { showToast(err.message, 'error'); @@ -155,11 +154,9 @@ function showCreateModal() { nameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doCreate(); }); } -// ==================== DETAIL VIEW ==================== - async function renderDetail(container, playlistId) { container.innerHTML = ` -
Loading...
+
${t('common.loading')}
`; try { @@ -168,8 +165,8 @@ async function renderDetail(container, playlistId) { } catch (err) { container.innerHTML = `
-

Failed to load playlist: ${esc(err.message)}

- Back to Playlists +

${t('playlist.load_failed', { error: esc(err.message) })}

+ ${t('playlist.back_to_playlists')}
`; } @@ -185,29 +182,29 @@ function renderDetailContent(container, playlist) {
-
Unpublished changes
-
${hasPublished ? 'Devices are still showing the last published version.' : 'This playlist has never been published. Devices will show nothing until you publish.'}
+
${t('playlist.draft.banner_title')}
+
${hasPublished ? t('playlist.draft.devices_showing_published') : t('playlist.draft.never_published')}
- ${hasPublished ? '' : ''} - + ${hasPublished ? `` : ''} +
` : ''} @@ -217,19 +214,18 @@ function renderDetailContent(container, playlist) { renderItems(playlist.items || []); - // Publish / Discard handlers const publishBtn = document.getElementById('publishBtn'); if (publishBtn) { publishBtn.addEventListener('click', async () => { try { publishBtn.disabled = true; - publishBtn.textContent = 'Publishing...'; + publishBtn.textContent = t('playlist.draft.publishing'); const updated = await api.publishPlaylist(playlist.id); - showToast('Playlist published — devices updated'); + showToast(t('playlist.toast.published')); renderDetailContent(container, updated); } catch (err) { publishBtn.disabled = false; - publishBtn.textContent = 'Publish'; + publishBtn.textContent = t('playlist.draft.publish'); showToast(err.message, 'error'); } }); @@ -237,10 +233,10 @@ function renderDetailContent(container, playlist) { const discardBtn = document.getElementById('discardDraftBtn'); if (discardBtn) { discardBtn.addEventListener('click', async () => { - if (!confirm('Discard all unpublished changes and revert to the last published version?')) return; + if (!confirm(t('playlist.confirm_discard_draft'))) return; try { const updated = await api.discardPlaylistDraft(playlist.id); - showToast('Draft changes discarded'); + showToast(t('playlist.toast.draft_discarded')); renderDetailContent(container, updated); } catch (err) { showToast(err.message, 'error'); @@ -248,19 +244,16 @@ function renderDetailContent(container, playlist) { }); } - // Inline rename document.getElementById('playlistTitle').addEventListener('click', () => inlineEdit(playlist, 'name')); document.getElementById('playlistDesc').addEventListener('click', () => inlineEdit(playlist, 'description')); - // Add content document.getElementById('addItemBtn').addEventListener('click', () => showAddItemModal(playlist.id)); - // Delete playlist document.getElementById('deletePlaylistBtn').addEventListener('click', async () => { - if (!confirm(`Delete "${playlist.name}"? This cannot be undone.`)) return; + if (!confirm(t('playlist.confirm_delete', { name: playlist.name }))) return; try { await api.deletePlaylist(playlist.id); - showToast('Playlist deleted'); + showToast(t('playlist.toast.deleted')); window.location.hash = '#/playlists'; } catch (err) { showToast(err.message, 'error'); @@ -268,7 +261,6 @@ function renderDetailContent(container, playlist) { }); } -// After any item mutation, re-fetch and re-render the full detail to update the draft banner async function refreshAfterMutation() { if (!currentPlaylistId) return; const mainContainer = document.getElementById('draftBanner')?.parentElement || document.querySelector('.page-header')?.parentElement; @@ -286,8 +278,8 @@ function renderItems(items) { if (!items.length) { itemsEl.innerHTML = `
-

This playlist is empty

-

Click "Add Content" to add items.

+

${t('playlist.items_empty')}

+

${t('playlist.items_empty_hint')}

`; return; @@ -303,29 +295,28 @@ function renderItems(items) { }
-
${esc(item.filename || item.widget_name || 'Unknown')}
-
${item.widget_id ? 'Widget' : esc(item.mime_type || 'Unknown type')}
+
${esc(item.filename || item.widget_name || t('common.unknown'))}
+
${item.widget_id ? t('playlist.item_widget') : esc(item.mime_type || t('playlist.unknown_type'))}
- + - sec + ${t('playlist.sec')}
- - -
`).join(''); - // Duration change handlers itemsEl.querySelectorAll('.item-duration').forEach(input => { input.addEventListener('change', async (e) => { const itemId = e.target.dataset.itemId; @@ -340,7 +331,6 @@ function renderItems(items) { }); }); - // Remove handlers itemsEl.querySelectorAll('.item-remove').forEach(btn => { btn.addEventListener('click', async (e) => { const itemId = e.currentTarget.dataset.itemId; @@ -349,14 +339,13 @@ function renderItems(items) { const playlist = await api.getPlaylist(currentPlaylistId); renderItems(playlist.items || []); refreshAfterMutation(); - showToast('Item removed'); + showToast(t('playlist.toast.item_removed')); } catch (err) { showToast(err.message, 'error'); } }); }); - // Up/down reorder (touch-friendly alternative to drag) itemsEl.querySelectorAll('.item-move').forEach(btn => { btn.addEventListener('click', async (e) => { if (btn.disabled) return; @@ -378,7 +367,6 @@ function renderItems(items) { }); }); - // Drag-to-reorder setupDragReorder(itemsEl); } @@ -413,10 +401,8 @@ function setupDragReorder(container) { const target = e.target.closest('.playlist-item'); if (!target || !dragEl || target === dragEl) return; - // Reorder DOM container.insertBefore(dragEl, target); - // Collect new order const order = Array.from(container.querySelectorAll('.playlist-item')) .map(el => parseInt(el.dataset.itemId, 10)); @@ -426,15 +412,12 @@ function setupDragReorder(container) { refreshAfterMutation(); } catch (err) { showToast(err.message, 'error'); - // Reload to fix state const playlist = await api.getPlaylist(currentPlaylistId); renderItems(playlist.items || []); } }); } -// ==================== INLINE EDIT ==================== - function inlineEdit(playlist, field) { const el = field === 'name' ? document.getElementById('playlistTitle') : document.getElementById('playlistDesc'); if (!el) return; @@ -464,7 +447,7 @@ function inlineEdit(playlist, field) { const newEl = document.createElement('h1'); newEl.id = 'playlistTitle'; newEl.style.cursor = 'pointer'; - newEl.title = 'Click to rename'; + newEl.title = t('playlist.click_to_rename'); newEl.textContent = playlist.name; input.replaceWith(newEl); newEl.addEventListener('click', () => inlineEdit(playlist, 'name')); @@ -492,11 +475,11 @@ function inlineEdit(playlist, field) { newEl.className = 'subtitle'; newEl.id = 'playlistDesc'; newEl.style.cursor = 'pointer'; - newEl.title = 'Click to edit description'; + newEl.title = t('playlist.click_to_edit_desc'); if (playlist.description) { newEl.textContent = playlist.description; } else { - newEl.innerHTML = 'Add a description...'; + newEl.innerHTML = `${t('playlist.add_desc_placeholder')}`; } input.replaceWith(newEl); newEl.addEventListener('click', () => inlineEdit(playlist, 'description')); @@ -507,22 +490,20 @@ function inlineEdit(playlist, field) { } } -// ==================== ADD ITEM MODAL ==================== - async function showAddItemModal(playlistId) { const modal = document.createElement('div'); modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000'; modal.innerHTML = `
-

Add Content to Playlist

+

${t('playlist.add_modal_title')}

- - + +
- +
- +
`; @@ -532,14 +513,13 @@ async function showAddItemModal(playlistId) { let allContent = []; let allWidgets = []; - // Load data try { [allContent, allWidgets] = await Promise.all([ api.getContent(), api.getWidgets ? api.getWidgets() : Promise.resolve([]) ]); } catch (err) { - document.getElementById('addItemList').innerHTML = `
Failed to load: ${esc(err.message)}
`; + document.getElementById('addItemList').innerHTML = `
${t('playlist.load_failed', { error: esc(err.message) })}
`; } function renderTab() { @@ -552,14 +532,14 @@ async function showAddItemModal(playlistId) { }); if (!filtered.length) { - list.innerHTML = `
No ${activeTab} found
`; + list.innerHTML = `
${activeTab === 'content' ? t('playlist.no_content_found') : t('playlist.no_widgets_found')}
`; return; } list.innerHTML = filtered.map(item => { const isWidget = activeTab === 'widgets'; - const name = item.filename || item.name || 'Unknown'; - const sub = isWidget ? (item.widget_type || 'Widget') : (item.mime_type || ''); + const name = item.filename || item.name || t('common.unknown'); + const sub = isWidget ? (item.widget_type || t('playlist.item_widget')) : (item.mime_type || ''); const thumb = item.thumbnail_path ? `/uploads/thumbnails/${esc(item.thumbnail_path.split('/').pop())}` : null; return `
@@ -570,12 +550,11 @@ async function showAddItemModal(playlistId) {
${esc(name)}
${esc(sub)}
- + `; }).join(''); - // Add button handlers list.querySelectorAll('.add-item-btn').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); @@ -584,23 +563,21 @@ async function showAddItemModal(playlistId) { const data = type === 'widget' ? { widget_id: id } : { content_id: id }; try { btn.disabled = true; - btn.textContent = 'Adding...'; + btn.textContent = t('playlist.adding'); await api.addPlaylistItem(playlistId, data); - btn.textContent = 'Added'; + btn.textContent = t('playlist.added'); btn.classList.remove('btn-primary'); btn.classList.add('btn-secondary'); - // Refresh the detail view (items + draft banner) refreshAfterMutation(); } catch (err) { btn.disabled = false; - btn.textContent = 'Add'; + btn.textContent = t('playlist.add_btn'); showToast(err.message, 'error'); } }); }); } - // Tab switching modal.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { activeTab = btn.dataset.tab; @@ -613,10 +590,8 @@ async function showAddItemModal(playlistId) { }); }); - // Search document.getElementById('addItemSearch').addEventListener('input', renderTab); - // Close document.getElementById('closeAddModal').addEventListener('click', () => modal.remove()); modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });