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 '--'; return new Date(ts * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); } function getTypeIcon(item) { if (item.widget_id) return ''; if (item.mime_type && item.mime_type.startsWith('video/')) return ''; return ''; } let currentPlaylistId = null; export function render(container) { const hash = window.location.hash; const match = hash.match(/#\/playlists\/(.+)/); if (match) { currentPlaylistId = match[1]; renderDetail(container, match[1]); } else { currentPlaylistId = null; renderList(container); } } export function cleanup() { currentPlaylistId = null; } let showAutoGenerated = true; async function renderList(container) { container.innerHTML = `
${t('common.loading')}
`; document.getElementById('createPlaylistBtn').addEventListener('click', showCreateModal); document.getElementById('showAutoToggle').addEventListener('change', (e) => { showAutoGenerated = e.target.checked; loadPlaylists(); }); loadPlaylists(); } async function loadPlaylists() { const grid = document.getElementById('playlistGrid'); if (!grid) return; try { const playlists = await api.getPlaylists(); if (!playlists.length) { grid.innerHTML = `

${t('playlist.empty_title')}

${t('playlist.empty_desc')}

`; return; } const filtered = showAutoGenerated ? playlists : playlists.filter(p => !p.is_auto_generated); if (!filtered.length) { grid.innerHTML = `
${playlists.length ? t('playlist.all_auto_generated') : ''}
`; return; } grid.innerHTML = filtered.map(p => `
${esc(p.name)}
${p.is_auto_generated ? `${t('playlist.tag_auto')}` : ''} ${p.status === 'draft' ? `${t('playlist.tag_draft')}` : ''}
${tn('playlist.item_count', p.item_count)}
${p.description ? `
${esc(p.description)}
` : ''}
${t('playlist.created_at', { date: formatDate(p.created_at) })} ${p.display_count ? `${tn('playlist.display_count', p.display_count)}` : ''}
`).join(''); } catch (err) { grid.innerHTML = `
${t('playlist.load_failed', { error: esc(err.message) })}
`; } } function showCreateModal() { 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 = `

${t('playlist.new_playlist')}

`; document.body.appendChild(modal); const nameInput = document.getElementById('newPlaylistName'); nameInput.focus(); document.getElementById('cancelCreateBtn').addEventListener('click', () => modal.remove()); modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); }); async function doCreate() { const name = nameInput.value.trim(); if (!name) { nameInput.focus(); return; } const desc = document.getElementById('newPlaylistDesc').value.trim(); try { const pl = await api.createPlaylist(name, desc); modal.remove(); showToast(t('playlist.toast.created')); window.location.hash = `#/playlists/${pl.id}`; } catch (err) { showToast(err.message, 'error'); } } document.getElementById('confirmCreateBtn').addEventListener('click', doCreate); nameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doCreate(); }); } async function renderDetail(container, playlistId) { container.innerHTML = `
${t('common.loading')}
`; try { const playlist = await api.getPlaylist(playlistId); renderDetailContent(container, playlist); } catch (err) { container.innerHTML = `

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

${t('playlist.back_to_playlists')}
`; } } function renderDetailContent(container, playlist) { const isDraft = playlist.status === 'draft'; const hasPublished = !!playlist.published_snapshot; container.innerHTML = ` ${isDraft ? `
${t('playlist.draft.banner_title')}
${hasPublished ? t('playlist.draft.devices_showing_published') : t('playlist.draft.never_published')}
${hasPublished ? `` : ''}
` : ''}
`; renderItems(playlist.items || []); const publishBtn = document.getElementById('publishBtn'); if (publishBtn) { publishBtn.addEventListener('click', async () => { try { publishBtn.disabled = true; publishBtn.textContent = t('playlist.draft.publishing'); const updated = await api.publishPlaylist(playlist.id); showToast(t('playlist.toast.published')); renderDetailContent(container, updated); } catch (err) { publishBtn.disabled = false; publishBtn.textContent = t('playlist.draft.publish'); showToast(err.message, 'error'); } }); } const discardBtn = document.getElementById('discardDraftBtn'); if (discardBtn) { discardBtn.addEventListener('click', async () => { if (!confirm(t('playlist.confirm_discard_draft'))) return; try { const updated = await api.discardPlaylistDraft(playlist.id); showToast(t('playlist.toast.draft_discarded')); renderDetailContent(container, updated); } catch (err) { showToast(err.message, 'error'); } }); } document.getElementById('playlistTitle').addEventListener('click', () => inlineEdit(playlist, 'name')); document.getElementById('playlistDesc').addEventListener('click', () => inlineEdit(playlist, 'description')); document.getElementById('addItemBtn').addEventListener('click', () => showAddItemModal(playlist.id)); document.getElementById('deletePlaylistBtn').addEventListener('click', async () => { if (!confirm(t('playlist.confirm_delete', { name: playlist.name }))) return; try { await api.deletePlaylist(playlist.id); showToast(t('playlist.toast.deleted')); window.location.hash = '#/playlists'; } catch (err) { showToast(err.message, 'error'); } }); } async function refreshAfterMutation() { if (!currentPlaylistId) return; const mainContainer = document.getElementById('draftBanner')?.parentElement || document.querySelector('.page-header')?.parentElement; if (!mainContainer) return; try { const playlist = await api.getPlaylist(currentPlaylistId); renderDetailContent(mainContainer, playlist); } catch (e) { /* silent */ } } function renderItems(items) { const itemsEl = document.getElementById('playlistItems'); if (!itemsEl) return; if (!items.length) { itemsEl.innerHTML = `

${t('playlist.items_empty')}

${t('playlist.items_empty_hint')}

`; return; } itemsEl.innerHTML = items.map((item, i) => `
${i + 1}
${item.thumbnail_path ? `` : `
${getTypeIcon(item)}
` }
${esc(item.filename || item.widget_name || t('common.unknown'))}
${item.widget_id ? t('playlist.item_widget') : esc(item.mime_type || t('playlist.unknown_type'))}
${t('playlist.sec')}
`).join(''); itemsEl.querySelectorAll('.item-duration').forEach(input => { input.addEventListener('change', async (e) => { const itemId = e.target.dataset.itemId; const val = parseInt(e.target.value, 10); if (!val || val < 1) { e.target.value = 10; return; } try { await api.updatePlaylistItem(currentPlaylistId, itemId, { duration_sec: val }); refreshAfterMutation(); } catch (err) { showToast(err.message, 'error'); } }); }); itemsEl.querySelectorAll('.item-remove').forEach(btn => { btn.addEventListener('click', async (e) => { const itemId = e.currentTarget.dataset.itemId; try { await api.deletePlaylistItem(currentPlaylistId, itemId); const playlist = await api.getPlaylist(currentPlaylistId); renderItems(playlist.items || []); refreshAfterMutation(); showToast(t('playlist.toast.item_removed')); } catch (err) { showToast(err.message, 'error'); } }); }); itemsEl.querySelectorAll('.item-move').forEach(btn => { btn.addEventListener('click', async (e) => { if (btn.disabled) return; const itemId = parseInt(e.currentTarget.dataset.itemId, 10); const dir = e.currentTarget.dataset.dir; const order = Array.from(itemsEl.querySelectorAll('.playlist-item')) .map(el => parseInt(el.dataset.itemId, 10)); const idx = order.indexOf(itemId); const swap = dir === 'up' ? idx - 1 : idx + 1; if (swap < 0 || swap >= order.length) return; [order[idx], order[swap]] = [order[swap], order[idx]]; try { const updated = await api.reorderPlaylistItems(currentPlaylistId, order); renderItems(updated); refreshAfterMutation(); } catch (err) { showToast(err.message, 'error'); } }); }); setupDragReorder(itemsEl); } function setupDragReorder(container) { let dragEl = null; container.addEventListener('dragstart', (e) => { dragEl = e.target.closest('.playlist-item'); if (!dragEl) return; dragEl.style.opacity = '0.4'; e.dataTransfer.effectAllowed = 'move'; }); container.addEventListener('dragend', () => { if (dragEl) dragEl.style.opacity = ''; dragEl = null; container.querySelectorAll('.playlist-item').forEach(el => el.style.borderTop = ''); }); container.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const target = e.target.closest('.playlist-item'); container.querySelectorAll('.playlist-item').forEach(el => el.style.borderTop = ''); if (target && target !== dragEl) { target.style.borderTop = '2px solid var(--primary)'; } }); container.addEventListener('drop', async (e) => { e.preventDefault(); const target = e.target.closest('.playlist-item'); if (!target || !dragEl || target === dragEl) return; container.insertBefore(dragEl, target); const order = Array.from(container.querySelectorAll('.playlist-item')) .map(el => parseInt(el.dataset.itemId, 10)); try { const items = await api.reorderPlaylistItems(currentPlaylistId, order); renderItems(items); refreshAfterMutation(); } catch (err) { showToast(err.message, 'error'); const playlist = await api.getPlaylist(currentPlaylistId); renderItems(playlist.items || []); } }); } function inlineEdit(playlist, field) { const el = field === 'name' ? document.getElementById('playlistTitle') : document.getElementById('playlistDesc'); if (!el) return; const current = playlist[field] || ''; const isName = field === 'name'; if (isName) { const input = document.createElement('input'); input.type = 'text'; input.className = 'input'; input.value = current; input.style.cssText = 'font-size:24px;font-weight:700;padding:2px 8px;width:100%'; el.replaceWith(input); input.focus(); input.select(); async function save() { const val = input.value.trim(); if (!val) { input.value = current; return; } try { const updated = await api.updatePlaylist(playlist.id, { [field]: val }); playlist[field] = updated[field]; } catch (err) { showToast(err.message, 'error'); } const newEl = document.createElement('h1'); newEl.id = 'playlistTitle'; newEl.style.cursor = 'pointer'; newEl.title = t('playlist.click_to_rename'); newEl.textContent = playlist.name; input.replaceWith(newEl); newEl.addEventListener('click', () => inlineEdit(playlist, 'name')); } input.addEventListener('blur', save); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') input.blur(); if (e.key === 'Escape') { input.value = current; input.blur(); } }); } else { const input = document.createElement('textarea'); input.className = 'input'; input.value = current; input.style.cssText = 'font-size:13px;padding:4px 8px;width:100%;height:40px;resize:vertical'; el.replaceWith(input); input.focus(); async function save() { const val = input.value.trim(); try { const updated = await api.updatePlaylist(playlist.id, { description: val }); playlist.description = updated.description; } catch (err) { showToast(err.message, 'error'); } const newEl = document.createElement('div'); newEl.className = 'subtitle'; newEl.id = 'playlistDesc'; newEl.style.cursor = 'pointer'; newEl.title = t('playlist.click_to_edit_desc'); if (playlist.description) { newEl.textContent = playlist.description; } else { newEl.innerHTML = `${t('playlist.add_desc_placeholder')}`; } input.replaceWith(newEl); newEl.addEventListener('click', () => inlineEdit(playlist, 'description')); } input.addEventListener('blur', save); input.addEventListener('keydown', (e) => { if (e.key === 'Escape') { input.value = current; input.blur(); } }); } } 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 = `

${t('playlist.add_modal_title')}

`; document.body.appendChild(modal); let activeTab = 'content'; let allContent = []; let allWidgets = []; try { [allContent, allWidgets] = await Promise.all([ api.getContent(), api.getWidgets ? api.getWidgets() : Promise.resolve([]) ]); } catch (err) { document.getElementById('addItemList').innerHTML = `
${t('playlist.load_failed', { error: esc(err.message) })}
`; } function renderTab() { const list = document.getElementById('addItemList'); const search = (document.getElementById('addItemSearch')?.value || '').toLowerCase(); const items = activeTab === 'content' ? allContent : allWidgets; const filtered = items.filter(item => { const name = (item.filename || item.name || '').toLowerCase(); return name.includes(search); }); if (!filtered.length) { 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 || t('common.unknown'); const sub = isWidget ? (item.widget_type || t('playlist.item_widget')) : (item.mime_type || ''); const thumb = item.thumbnail_path ? `/api/content/${esc(item.id)}/thumbnail` : null; return `
${thumb ? `` : '
'}
${esc(name)}
${esc(sub)}
`; }).join(''); list.querySelectorAll('.add-item-btn').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const id = btn.dataset.id; const type = btn.dataset.type; const data = type === 'widget' ? { widget_id: id } : { content_id: id }; try { btn.disabled = true; btn.textContent = t('playlist.adding'); await api.addPlaylistItem(playlistId, data); btn.textContent = t('playlist.added'); btn.classList.remove('btn-primary'); btn.classList.add('btn-secondary'); refreshAfterMutation(); } catch (err) { btn.disabled = false; btn.textContent = t('playlist.add_btn'); showToast(err.message, 'error'); } }); }); } modal.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { activeTab = btn.dataset.tab; modal.querySelectorAll('.tab-btn').forEach(b => { b.classList.toggle('btn-primary', b.dataset.tab === activeTab); b.classList.toggle('btn-secondary', b.dataset.tab !== activeTab); b.classList.toggle('active', b.dataset.tab === activeTab); }); renderTab(); }); }); document.getElementById('addItemSearch').addEventListener('input', renderTab); document.getElementById('closeAddModal').addEventListener('click', () => modal.remove()); modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); }); renderTab(); }