mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
i18n batch 3a: wire playlists.js (~65 keys)
- List view: tags, item/display pluralization, empty state, load errors - Detail view: draft banner, inline rename/description, items list - Drag-reorder + up/down buttons, duration editor - Add-item modal with content/widgets tabs and search - 671 keys total, parity 100% across en/es/fr/de/pt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
103803fb92
commit
04891bccee
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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é',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Playlists</h1>
|
||||
<div class="subtitle">Create and manage content playlists</div>
|
||||
<h1>${t('playlist.title')}</h1>
|
||||
<div class="subtitle">${t('playlist.subtitle')}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text-secondary);cursor:pointer">
|
||||
<input type="checkbox" id="showAutoToggle" ${showAutoGenerated ? 'checked' : ''}>
|
||||
Show auto-generated
|
||||
${t('playlist.show_auto_generated')}
|
||||
</label>
|
||||
<button class="btn btn-primary" id="createPlaylistBtn">+ New Playlist</button>
|
||||
<button class="btn btn-primary" id="createPlaylistBtn">${t('playlist.new_playlist_btn')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="playlistGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px">
|
||||
<div style="color:var(--text-muted);padding:40px;text-align:center">Loading...</div>
|
||||
<div style="color:var(--text-muted);padding:40px;text-align:center">${t('common.loading')}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -76,8 +75,8 @@ async function loadPlaylists() {
|
|||
<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||
</svg>
|
||||
<h3 style="margin-bottom:8px;color:var(--text-primary)">No playlists yet</h3>
|
||||
<p>Create your first playlist to organize content for your displays.</p>
|
||||
<h3 style="margin-bottom:8px;color:var(--text-primary)">${t('playlist.empty_title')}</h3>
|
||||
<p>${t('playlist.empty_desc')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
|
|
@ -87,7 +86,7 @@ async function loadPlaylists() {
|
|||
if (!filtered.length) {
|
||||
grid.innerHTML = `
|
||||
<div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--text-muted)">
|
||||
${playlists.length ? 'All playlists are auto-generated. Toggle "Show auto-generated" to see them.' : ''}
|
||||
${playlists.length ? t('playlist.all_auto_generated') : ''}
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
|
|
@ -98,20 +97,20 @@ async function loadPlaylists() {
|
|||
<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:12px">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<div style="font-size:16px;font-weight:600;color:var(--text-primary)">${esc(p.name)}</div>
|
||||
${p.is_auto_generated ? '<span style="font-size:10px;padding:2px 6px;border-radius:4px;background:var(--bg-input);color:var(--text-muted)">auto</span>' : ''}
|
||||
${p.status === 'draft' ? '<span style="font-size:10px;padding:2px 6px;border-radius:4px;background:#78350f;color:#fbbf24">draft</span>' : ''}
|
||||
${p.is_auto_generated ? `<span style="font-size:10px;padding:2px 6px;border-radius:4px;background:var(--bg-input);color:var(--text-muted)">${t('playlist.tag_auto')}</span>` : ''}
|
||||
${p.status === 'draft' ? `<span style="font-size:10px;padding:2px 6px;border-radius:4px;background:#78350f;color:#fbbf24">${t('playlist.tag_draft')}</span>` : ''}
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);white-space:nowrap;margin-left:12px">${p.item_count} item${p.item_count !== 1 ? 's' : ''}</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);white-space:nowrap;margin-left:12px">${tn('playlist.item_count', p.item_count)}</div>
|
||||
</div>
|
||||
${p.description ? `<div style="font-size:13px;color:var(--text-secondary);margin-bottom:12px;line-height:1.4">${esc(p.description)}</div>` : ''}
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--text-muted)">
|
||||
<span>Created ${formatDate(p.created_at)}</span>
|
||||
${p.display_count ? `<span>${p.display_count} display${p.display_count !== 1 ? 's' : ''}</span>` : ''}
|
||||
<span>${t('playlist.created_at', { date: formatDate(p.created_at) })}</span>
|
||||
${p.display_count ? `<span>${tn('playlist.display_count', p.display_count)}</span>` : ''}
|
||||
</div>
|
||||
</a>
|
||||
`).join('');
|
||||
} catch (err) {
|
||||
grid.innerHTML = `<div style="grid-column:1/-1;color:var(--text-muted);padding:40px;text-align:center">Failed to load playlists: ${esc(err.message)}</div>`;
|
||||
grid.innerHTML = `<div style="grid-column:1/-1;color:var(--text-muted);padding:40px;text-align:center">${t('playlist.load_failed', { error: esc(err.message) })}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 = `
|
||||
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px;width:400px;max-width:90vw">
|
||||
<h3 style="margin-bottom:16px;color:var(--text-primary)">New Playlist</h3>
|
||||
<input type="text" id="newPlaylistName" class="input" placeholder="Playlist name" style="width:100%;margin-bottom:12px" autofocus>
|
||||
<textarea id="newPlaylistDesc" class="input" placeholder="Description (optional)" style="width:100%;height:60px;resize:vertical;margin-bottom:16px"></textarea>
|
||||
<h3 style="margin-bottom:16px;color:var(--text-primary)">${t('playlist.new_playlist')}</h3>
|
||||
<input type="text" id="newPlaylistName" class="input" placeholder="${t('playlist.name_placeholder')}" style="width:100%;margin-bottom:12px" autofocus>
|
||||
<textarea id="newPlaylistDesc" class="input" placeholder="${t('playlist.desc_placeholder')}" style="width:100%;height:60px;resize:vertical;margin-bottom:16px"></textarea>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button class="btn btn-secondary" id="cancelCreateBtn">Cancel</button>
|
||||
<button class="btn btn-primary" id="confirmCreateBtn">Create</button>
|
||||
<button class="btn btn-secondary" id="cancelCreateBtn">${t('common.cancel')}</button>
|
||||
<button class="btn btn-primary" id="confirmCreateBtn">${t('playlist.create_btn')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -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 = `
|
||||
<div style="color:var(--text-muted);padding:40px;text-align:center">Loading...</div>
|
||||
<div style="color:var(--text-muted);padding:40px;text-align:center">${t('common.loading')}</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
|
|
@ -168,8 +165,8 @@ async function renderDetail(container, playlistId) {
|
|||
} catch (err) {
|
||||
container.innerHTML = `
|
||||
<div style="padding:40px;text-align:center;color:var(--text-muted)">
|
||||
<p>Failed to load playlist: ${esc(err.message)}</p>
|
||||
<a href="#/playlists" class="btn btn-secondary" style="margin-top:16px">Back to Playlists</a>
|
||||
<p>${t('playlist.load_failed', { error: esc(err.message) })}</p>
|
||||
<a href="#/playlists" class="btn btn-secondary" style="margin-top:16px">${t('playlist.back_to_playlists')}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -185,29 +182,29 @@ function renderDetailContent(container, playlist) {
|
|||
<div style="display:flex;align-items:center;gap:10px;color:#fbbf24">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:14px">Unpublished changes</div>
|
||||
<div style="font-size:12px;color:#fcd34d;opacity:0.85">${hasPublished ? 'Devices are still showing the last published version.' : 'This playlist has never been published. Devices will show nothing until you publish.'}</div>
|
||||
<div style="font-weight:600;font-size:14px">${t('playlist.draft.banner_title')}</div>
|
||||
<div style="font-size:12px;color:#fcd34d;opacity:0.85">${hasPublished ? t('playlist.draft.devices_showing_published') : t('playlist.draft.never_published')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-shrink:0">
|
||||
${hasPublished ? '<button class="btn btn-secondary btn-sm" id="discardDraftBtn" style="color:#fbbf24;border-color:#92400e">Discard Changes</button>' : ''}
|
||||
<button class="btn btn-sm" id="publishBtn" style="background:#f59e0b;color:#000;font-weight:600;border:none">Publish</button>
|
||||
${hasPublished ? `<button class="btn btn-secondary btn-sm" id="discardDraftBtn" style="color:#fbbf24;border-color:#92400e">${t('playlist.draft.discard_changes')}</button>` : ''}
|
||||
<button class="btn btn-sm" id="publishBtn" style="background:#f59e0b;color:#000;font-weight:600;border:none">${t('playlist.draft.publish')}</button>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="page-header">
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<a href="#/playlists" style="color:var(--text-muted);text-decoration:none;font-size:20px" title="Back">←</a>
|
||||
<a href="#/playlists" style="color:var(--text-muted);text-decoration:none;font-size:20px" title="${t('playlist.back')}">←</a>
|
||||
<div>
|
||||
<h1 id="playlistTitle" style="cursor:pointer" title="Click to rename">${esc(playlist.name)}</h1>
|
||||
<div class="subtitle" id="playlistDesc" style="cursor:pointer" title="Click to edit description">${playlist.description ? esc(playlist.description) : '<span style="opacity:0.5">Add a description...</span>'}</div>
|
||||
${playlist.display_count ? `<div style="font-size:12px;color:var(--text-muted);margin-top:4px">Assigned to ${playlist.display_count} display${playlist.display_count !== 1 ? 's' : ''}</div>` : ''}
|
||||
<h1 id="playlistTitle" style="cursor:pointer" title="${t('playlist.click_to_rename')}">${esc(playlist.name)}</h1>
|
||||
<div class="subtitle" id="playlistDesc" style="cursor:pointer" title="${t('playlist.click_to_edit_desc')}">${playlist.description ? esc(playlist.description) : `<span style="opacity:0.5">${t('playlist.add_desc_placeholder')}</span>`}</div>
|
||||
${playlist.display_count ? `<div style="font-size:12px;color:var(--text-muted);margin-top:4px">${tn('playlist.assigned_to', playlist.display_count)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-primary" id="addItemBtn">+ Add Content</button>
|
||||
<button class="btn btn-secondary" id="deletePlaylistBtn" style="color:var(--danger)">Delete Playlist</button>
|
||||
<button class="btn btn-primary" id="addItemBtn">${t('playlist.add_content')}</button>
|
||||
<button class="btn btn-secondary" id="deletePlaylistBtn" style="color:var(--danger)">${t('playlist.delete_playlist')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -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 = `
|
||||
<div style="text-align:center;padding:40px;color:var(--text-muted);border:2px dashed var(--border);border-radius:var(--radius-lg)">
|
||||
<p style="margin-bottom:8px">This playlist is empty</p>
|
||||
<p style="font-size:13px">Click "Add Content" to add items.</p>
|
||||
<p style="margin-bottom:8px">${t('playlist.items_empty')}</p>
|
||||
<p style="font-size:13px">${t('playlist.items_empty_hint')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
|
|
@ -303,29 +295,28 @@ function renderItems(items) {
|
|||
}
|
||||
</div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(item.filename || item.widget_name || 'Unknown')}</div>
|
||||
<div style="font-size:12px;color:var(--text-muted)">${item.widget_id ? 'Widget' : esc(item.mime_type || 'Unknown type')}</div>
|
||||
<div style="font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(item.filename || item.widget_name || t('common.unknown'))}</div>
|
||||
<div style="font-size:12px;color:var(--text-muted)">${item.widget_id ? t('playlist.item_widget') : esc(item.mime_type || t('playlist.unknown_type'))}</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
|
||||
<label style="font-size:12px;color:var(--text-muted)">Duration</label>
|
||||
<label style="font-size:12px;color:var(--text-muted)">${t('playlist.duration')}</label>
|
||||
<input type="number" class="input item-duration" data-item-id="${item.id}" value="${item.duration_sec}" min="1" style="width:60px;padding:4px 8px;font-size:13px;text-align:center">
|
||||
<span style="font-size:12px;color:var(--text-muted)">sec</span>
|
||||
<span style="font-size:12px;color:var(--text-muted)">${t('playlist.sec')}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
|
||||
<button class="btn-icon item-move" data-item-id="${item.id}" data-dir="up" title="Move up" aria-label="Move up" ${i === 0 ? 'disabled' : ''} style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px;${i === 0 ? 'opacity:0.3;cursor:not-allowed' : ''}">
|
||||
<button class="btn-icon item-move" data-item-id="${item.id}" data-dir="up" title="${t('playlist.move_up')}" aria-label="${t('playlist.move_up')}" ${i === 0 ? 'disabled' : ''} style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px;${i === 0 ? 'opacity:0.3;cursor:not-allowed' : ''}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>
|
||||
</button>
|
||||
<button class="btn-icon item-move" data-item-id="${item.id}" data-dir="down" title="Move down" aria-label="Move down" ${i === items.length - 1 ? 'disabled' : ''} style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px;${i === items.length - 1 ? 'opacity:0.3;cursor:not-allowed' : ''}">
|
||||
<button class="btn-icon item-move" data-item-id="${item.id}" data-dir="down" title="${t('playlist.move_down')}" aria-label="${t('playlist.move_down')}" ${i === items.length - 1 ? 'disabled' : ''} style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px;${i === items.length - 1 ? 'opacity:0.3;cursor:not-allowed' : ''}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<button class="btn-icon item-remove" data-item-id="${item.id}" title="Remove" aria-label="Remove item" style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px">
|
||||
<button class="btn-icon item-remove" data-item-id="${item.id}" title="${t('common.delete')}" aria-label="${t('playlist.remove_item')}" style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).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 = '<span style="opacity:0.5">Add a description...</span>';
|
||||
newEl.innerHTML = `<span style="opacity:0.5">${t('playlist.add_desc_placeholder')}</span>`;
|
||||
}
|
||||
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 = `
|
||||
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px;max-width:560px;width:95vw;max-height:80vh;display:flex;flex-direction:column">
|
||||
<h3 style="margin-bottom:16px;color:var(--text-primary)">Add Content to Playlist</h3>
|
||||
<h3 style="margin-bottom:16px;color:var(--text-primary)">${t('playlist.add_modal_title')}</h3>
|
||||
<div style="display:flex;gap:8px;margin-bottom:12px">
|
||||
<button class="btn btn-primary btn-sm tab-btn active" data-tab="content">Content</button>
|
||||
<button class="btn btn-secondary btn-sm tab-btn" data-tab="widgets">Widgets</button>
|
||||
<button class="btn btn-primary btn-sm tab-btn active" data-tab="content">${t('playlist.tab_content')}</button>
|
||||
<button class="btn btn-secondary btn-sm tab-btn" data-tab="widgets">${t('playlist.tab_widgets')}</button>
|
||||
</div>
|
||||
<input type="text" id="addItemSearch" class="input" placeholder="Search..." style="width:100%;margin-bottom:12px">
|
||||
<input type="text" id="addItemSearch" class="input" placeholder="${t('playlist.search_placeholder')}" style="width:100%;margin-bottom:12px">
|
||||
<div id="addItemList" style="flex:1;overflow-y:auto;min-height:200px;max-height:400px"></div>
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:16px">
|
||||
<button class="btn btn-secondary" id="closeAddModal">Close</button>
|
||||
<button class="btn btn-secondary" id="closeAddModal">${t('playlist.close')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -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 = `<div style="color:var(--text-muted);padding:20px;text-align:center">Failed to load: ${esc(err.message)}</div>`;
|
||||
document.getElementById('addItemList').innerHTML = `<div style="color:var(--text-muted);padding:20px;text-align:center">${t('playlist.load_failed', { error: esc(err.message) })}</div>`;
|
||||
}
|
||||
|
||||
function renderTab() {
|
||||
|
|
@ -552,14 +532,14 @@ async function showAddItemModal(playlistId) {
|
|||
});
|
||||
|
||||
if (!filtered.length) {
|
||||
list.innerHTML = `<div style="color:var(--text-muted);padding:20px;text-align:center">No ${activeTab} found</div>`;
|
||||
list.innerHTML = `<div style="color:var(--text-muted);padding:20px;text-align:center">${activeTab === 'content' ? t('playlist.no_content_found') : t('playlist.no_widgets_found')}</div>`;
|
||||
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 `
|
||||
<div class="add-item-row" data-id="${esc(item.id)}" data-type="${isWidget ? 'widget' : 'content'}" style="display:flex;align-items:center;gap:12px;padding:10px;border-radius:var(--radius);cursor:pointer;transition:background 0.1s">
|
||||
|
|
@ -570,12 +550,11 @@ async function showAddItemModal(playlistId) {
|
|||
<div style="font-size:13px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(name)}</div>
|
||||
<div style="font-size:11px;color:var(--text-muted)">${esc(sub)}</div>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm add-item-btn" data-id="${esc(item.id)}" data-type="${isWidget ? 'widget' : 'content'}">Add</button>
|
||||
<button class="btn btn-primary btn-sm add-item-btn" data-id="${esc(item.id)}" data-type="${isWidget ? 'widget' : 'content'}">${t('playlist.add_btn')}</button>
|
||||
</div>
|
||||
`;
|
||||
}).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(); });
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue