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 = `
`;
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 = `
`;
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 = `
`;
}
}
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
? `
.pop())})
`
: `
${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 ? `/uploads/thumbnails/${esc(item.thumbnail_path.split('/').pop())}` : null;
return `
${thumb ? `

` : '
'}
`;
}).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();
}