mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
commit
647a7de1e6
|
|
@ -146,6 +146,7 @@ export const api = {
|
||||||
addPlaylistItem: (id, data) => request(`/playlists/${id}/items`, { method: 'POST', body: JSON.stringify(data) }),
|
addPlaylistItem: (id, data) => request(`/playlists/${id}/items`, { method: 'POST', body: JSON.stringify(data) }),
|
||||||
updatePlaylistItem: (id, itemId, data) => request(`/playlists/${id}/items/${itemId}`, { method: 'PUT', body: JSON.stringify(data) }),
|
updatePlaylistItem: (id, itemId, data) => request(`/playlists/${id}/items/${itemId}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
deletePlaylistItem: (id, itemId) => request(`/playlists/${id}/items/${itemId}`, { method: 'DELETE' }),
|
deletePlaylistItem: (id, itemId) => request(`/playlists/${id}/items/${itemId}`, { method: 'DELETE' }),
|
||||||
|
duplicatePlaylistItem: (id, itemId) => request(`/playlists/${id}/items/${itemId}/duplicate`, { method: 'POST' }),
|
||||||
reorderPlaylistItems: (id, order) => request(`/playlists/${id}/items/reorder`, { method: 'POST', body: JSON.stringify({ order }) }),
|
reorderPlaylistItems: (id, order) => request(`/playlists/${id}/items/reorder`, { method: 'POST', body: JSON.stringify({ order }) }),
|
||||||
// #74/#75 per-item schedule blocks
|
// #74/#75 per-item schedule blocks
|
||||||
getItemSchedules: (id, itemId) => request(`/playlists/${id}/items/${itemId}/schedules`),
|
getItemSchedules: (id, itemId) => request(`/playlists/${id}/items/${itemId}/schedules`),
|
||||||
|
|
|
||||||
|
|
@ -729,6 +729,13 @@ export default {
|
||||||
'playlist.no_content_found': 'Kein Inhalt gefunden',
|
'playlist.no_content_found': 'Kein Inhalt gefunden',
|
||||||
'playlist.no_widgets_found': 'Keine Widgets gefunden',
|
'playlist.no_widgets_found': 'Keine Widgets gefunden',
|
||||||
'playlist.add_btn': 'Hinzufügen',
|
'playlist.add_btn': 'Hinzufügen',
|
||||||
|
'playlist.replace_item': 'Inhalt ersetzen',
|
||||||
|
'playlist.duplicate_item': 'Element duplizieren',
|
||||||
|
'playlist.replace_modal_title': 'Inhalt ersetzen',
|
||||||
|
'playlist.replace_btn': 'Ersetzen',
|
||||||
|
'playlist.replacing': 'Wird ersetzt…',
|
||||||
|
'playlist.toast.item_duplicated': 'Element dupliziert',
|
||||||
|
'playlist.toast.item_replaced': 'Inhalt ersetzt',
|
||||||
'playlist.adding': 'Wird hinzugefügt...',
|
'playlist.adding': 'Wird hinzugefügt...',
|
||||||
'playlist.added': 'Hinzugefügt',
|
'playlist.added': 'Hinzugefügt',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -813,6 +813,13 @@ export default {
|
||||||
'playlist.no_content_found': 'No content found',
|
'playlist.no_content_found': 'No content found',
|
||||||
'playlist.no_widgets_found': 'No widgets found',
|
'playlist.no_widgets_found': 'No widgets found',
|
||||||
'playlist.add_btn': 'Add',
|
'playlist.add_btn': 'Add',
|
||||||
|
'playlist.replace_item': 'Replace content',
|
||||||
|
'playlist.duplicate_item': 'Duplicate item',
|
||||||
|
'playlist.replace_modal_title': 'Replace content',
|
||||||
|
'playlist.replace_btn': 'Replace',
|
||||||
|
'playlist.replacing': 'Replacing…',
|
||||||
|
'playlist.toast.item_duplicated': 'Item duplicated',
|
||||||
|
'playlist.toast.item_replaced': 'Content replaced',
|
||||||
'playlist.adding': 'Adding...',
|
'playlist.adding': 'Adding...',
|
||||||
'playlist.added': 'Added',
|
'playlist.added': 'Added',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -728,6 +728,13 @@ export default {
|
||||||
'playlist.no_content_found': 'No se encontró contenido',
|
'playlist.no_content_found': 'No se encontró contenido',
|
||||||
'playlist.no_widgets_found': 'No se encontraron widgets',
|
'playlist.no_widgets_found': 'No se encontraron widgets',
|
||||||
'playlist.add_btn': 'Agregar',
|
'playlist.add_btn': 'Agregar',
|
||||||
|
'playlist.replace_item': 'Reemplazar contenido',
|
||||||
|
'playlist.duplicate_item': 'Duplicar elemento',
|
||||||
|
'playlist.replace_modal_title': 'Reemplazar contenido',
|
||||||
|
'playlist.replace_btn': 'Reemplazar',
|
||||||
|
'playlist.replacing': 'Reemplazando…',
|
||||||
|
'playlist.toast.item_duplicated': 'Elemento duplicado',
|
||||||
|
'playlist.toast.item_replaced': 'Contenido reemplazado',
|
||||||
'playlist.adding': 'Agregando...',
|
'playlist.adding': 'Agregando...',
|
||||||
'playlist.added': 'Agregado',
|
'playlist.added': 'Agregado',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -729,6 +729,13 @@ export default {
|
||||||
'playlist.no_content_found': 'Aucun contenu trouvé',
|
'playlist.no_content_found': 'Aucun contenu trouvé',
|
||||||
'playlist.no_widgets_found': 'Aucun widget trouvé',
|
'playlist.no_widgets_found': 'Aucun widget trouvé',
|
||||||
'playlist.add_btn': 'Ajouter',
|
'playlist.add_btn': 'Ajouter',
|
||||||
|
'playlist.replace_item': 'Remplacer le contenu',
|
||||||
|
'playlist.duplicate_item': 'Dupliquer l’élément',
|
||||||
|
'playlist.replace_modal_title': 'Remplacer le contenu',
|
||||||
|
'playlist.replace_btn': 'Remplacer',
|
||||||
|
'playlist.replacing': 'Remplacement…',
|
||||||
|
'playlist.toast.item_duplicated': 'Élément dupliqué',
|
||||||
|
'playlist.toast.item_replaced': 'Contenu remplacé',
|
||||||
'playlist.adding': 'Ajout...',
|
'playlist.adding': 'Ajout...',
|
||||||
'playlist.added': 'Ajouté',
|
'playlist.added': 'Ajouté',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -724,6 +724,13 @@ export default {
|
||||||
'playlist.no_content_found': 'Nessun contenuto trovato',
|
'playlist.no_content_found': 'Nessun contenuto trovato',
|
||||||
'playlist.no_widgets_found': 'Nessun widget trovato',
|
'playlist.no_widgets_found': 'Nessun widget trovato',
|
||||||
'playlist.add_btn': 'Aggiungi',
|
'playlist.add_btn': 'Aggiungi',
|
||||||
|
'playlist.replace_item': 'Sostituisci contenuto',
|
||||||
|
'playlist.duplicate_item': 'Duplica elemento',
|
||||||
|
'playlist.replace_modal_title': 'Sostituisci contenuto',
|
||||||
|
'playlist.replace_btn': 'Sostituisci',
|
||||||
|
'playlist.replacing': 'Sostituzione…',
|
||||||
|
'playlist.toast.item_duplicated': 'Elemento duplicato',
|
||||||
|
'playlist.toast.item_replaced': 'Contenuto sostituito',
|
||||||
'playlist.adding': 'Aggiunta in corso...',
|
'playlist.adding': 'Aggiunta in corso...',
|
||||||
'playlist.added': 'Aggiunto',
|
'playlist.added': 'Aggiunto',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -729,6 +729,13 @@ export default {
|
||||||
'playlist.no_content_found': 'Nenhum conteúdo encontrado',
|
'playlist.no_content_found': 'Nenhum conteúdo encontrado',
|
||||||
'playlist.no_widgets_found': 'Nenhum widget encontrado',
|
'playlist.no_widgets_found': 'Nenhum widget encontrado',
|
||||||
'playlist.add_btn': 'Adicionar',
|
'playlist.add_btn': 'Adicionar',
|
||||||
|
'playlist.replace_item': 'Substituir conteúdo',
|
||||||
|
'playlist.duplicate_item': 'Duplicar item',
|
||||||
|
'playlist.replace_modal_title': 'Substituir conteúdo',
|
||||||
|
'playlist.replace_btn': 'Substituir',
|
||||||
|
'playlist.replacing': 'Substituindo…',
|
||||||
|
'playlist.toast.item_duplicated': 'Item duplicado',
|
||||||
|
'playlist.toast.item_replaced': 'Conteúdo substituído',
|
||||||
'playlist.adding': 'Adicionando...',
|
'playlist.adding': 'Adicionando...',
|
||||||
'playlist.added': 'Adicionado',
|
'playlist.added': 'Adicionado',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,12 @@ function renderItems(items) {
|
||||||
<button class="btn-icon item-schedule" data-item-id="${item.id}" title="${t('itemsched.title')}" aria-label="${t('itemsched.title')}" style="color:${item.schedules && item.schedules.length ? '#38bdf8' : 'var(--text-muted)'};background:none;border:none;cursor:pointer;padding:4px;border-radius:4px">
|
<button class="btn-icon item-schedule" data-item-id="${item.id}" title="${t('itemsched.title')}" aria-label="${t('itemsched.title')}" style="color:${item.schedules && item.schedules.length ? '#38bdf8' : '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"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 14"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 14"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-icon item-replace" data-item-id="${item.id}" title="${t('playlist.replace_item')}" aria-label="${t('playlist.replace_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"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon item-duplicate" data-item-id="${item.id}" title="${t('playlist.duplicate_item')}" aria-label="${t('playlist.duplicate_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"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||||
|
</button>
|
||||||
<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' : ''}">
|
<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>
|
<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>
|
||||||
|
|
@ -393,6 +399,32 @@ function renderItems(items) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #105 duplicate: server copies the row + its schedule blocks, appended at the end.
|
||||||
|
itemsEl.querySelectorAll('.item-duplicate').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
const itemId = e.currentTarget.dataset.itemId;
|
||||||
|
try {
|
||||||
|
e.currentTarget.disabled = true;
|
||||||
|
await api.duplicatePlaylistItem(currentPlaylistId, itemId);
|
||||||
|
const playlist = await api.getPlaylist(currentPlaylistId);
|
||||||
|
renderItems(playlist.items || []);
|
||||||
|
refreshAfterMutation();
|
||||||
|
showToast(t('playlist.toast.item_duplicated'));
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// #105 replace: reuse the add-item picker in "replace" mode — swaps content/widget
|
||||||
|
// in place, preserving duration/schedule/zone (server-side).
|
||||||
|
itemsEl.querySelectorAll('.item-replace').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const itemId = e.currentTarget.dataset.itemId;
|
||||||
|
showAddItemModal(currentPlaylistId, { replaceItemId: itemId });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
itemsEl.querySelectorAll('.item-move').forEach(btn => {
|
itemsEl.querySelectorAll('.item-move').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
if (btn.disabled) return;
|
if (btn.disabled) return;
|
||||||
|
|
@ -537,12 +569,15 @@ function inlineEdit(playlist, field) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showAddItemModal(playlistId) {
|
async function showAddItemModal(playlistId, opts = {}) {
|
||||||
|
// #105: when opts.replaceItemId is set, picking an item REPLACES that item's
|
||||||
|
// content/widget in place (preserving duration/schedule/zone) instead of adding.
|
||||||
|
const replaceItemId = opts.replaceItemId || null;
|
||||||
const modal = document.createElement('div');
|
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.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 = `
|
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">
|
<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)">${t('playlist.add_modal_title')}</h3>
|
<h3 style="margin-bottom:16px;color:var(--text-primary)">${replaceItemId ? t('playlist.replace_modal_title') : t('playlist.add_modal_title')}</h3>
|
||||||
<div style="display:flex;gap:8px;margin-bottom:12px">
|
<div style="display:flex;gap:8px;margin-bottom:12px">
|
||||||
<button class="btn btn-primary btn-sm tab-btn active" data-tab="content">${t('playlist.tab_content')}</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>
|
<button class="btn btn-secondary btn-sm tab-btn" data-tab="widgets">${t('playlist.tab_widgets')}</button>
|
||||||
|
|
@ -597,7 +632,7 @@ 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: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 style="font-size:11px;color:var(--text-muted)">${esc(sub)}</div>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<button class="btn btn-primary btn-sm add-item-btn" data-id="${esc(item.id)}" data-type="${isWidget ? 'widget' : 'content'}">${replaceItemId ? t('playlist.replace_btn') : t('playlist.add_btn')}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
@ -610,6 +645,18 @@ async function showAddItemModal(playlistId) {
|
||||||
const data = type === 'widget' ? { widget_id: id } : { content_id: id };
|
const data = type === 'widget' ? { widget_id: id } : { content_id: id };
|
||||||
try {
|
try {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
if (replaceItemId) {
|
||||||
|
btn.textContent = t('playlist.replacing');
|
||||||
|
// PUT supports a content/widget swap; the server nulls the opposite FK and
|
||||||
|
// preserves duration/schedule/zone. Close on success and re-render the list.
|
||||||
|
await api.updatePlaylistItem(playlistId, replaceItemId, data);
|
||||||
|
modal.remove();
|
||||||
|
const playlist = await api.getPlaylist(playlistId);
|
||||||
|
renderItems(playlist.items || []);
|
||||||
|
refreshAfterMutation();
|
||||||
|
showToast(t('playlist.toast.item_replaced'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
btn.textContent = t('playlist.adding');
|
btn.textContent = t('playlist.adding');
|
||||||
await api.addPlaylistItem(playlistId, data);
|
await api.addPlaylistItem(playlistId, data);
|
||||||
btn.textContent = t('playlist.added');
|
btn.textContent = t('playlist.added');
|
||||||
|
|
@ -618,7 +665,7 @@ async function showAddItemModal(playlistId) {
|
||||||
refreshAfterMutation();
|
refreshAfterMutation();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = t('playlist.add_btn');
|
btn.textContent = replaceItemId ? t('playlist.replace_btn') : t('playlist.add_btn');
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,37 @@ router.put('/:id/items/:itemId', requirePlaylistWrite, (req, res) => {
|
||||||
values.push(duration_sec);
|
values.push(duration_sec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #105 replace: swap the item's content/widget in place while preserving zone_id,
|
||||||
|
// duration, sort_order and schedule rows. playlist_items is normalized (no
|
||||||
|
// type-specific columns — mime_type/remote_url/filepath/widget_type are JOINed at
|
||||||
|
// read time), so this is a clean FK swap across ANY content type (image<->video<->
|
||||||
|
// youtube<->widget). Exactly one of content_id/widget_id ends up set; the other is
|
||||||
|
// nulled. Only acts when the request explicitly carries content_id or widget_id, so
|
||||||
|
// partial PUTs (duration/zone/sort) are unaffected.
|
||||||
|
const replacingContent = Object.prototype.hasOwnProperty.call(req.body, 'content_id');
|
||||||
|
const replacingWidget = Object.prototype.hasOwnProperty.call(req.body, 'widget_id');
|
||||||
|
if (replacingContent || replacingWidget) {
|
||||||
|
const newContentId = replacingContent ? req.body.content_id : null;
|
||||||
|
const newWidgetId = replacingWidget ? req.body.widget_id : null;
|
||||||
|
if (!newContentId && !newWidgetId) return res.status(400).json({ error: 'content_id or widget_id required to replace' });
|
||||||
|
if (newContentId && newWidgetId) return res.status(400).json({ error: 'provide only one of content_id / widget_id' });
|
||||||
|
if (newContentId) {
|
||||||
|
const content = db.prepare('SELECT id, workspace_id FROM content WHERE id = ?').get(newContentId);
|
||||||
|
if (!content) return res.status(404).json({ error: 'Content not found' });
|
||||||
|
if (content.workspace_id && content.workspace_id !== req.playlist.workspace_id) {
|
||||||
|
return res.status(403).json({ error: 'Content is not in this playlist\'s workspace' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const widget = db.prepare('SELECT id, workspace_id FROM widgets WHERE id = ?').get(newWidgetId);
|
||||||
|
if (!widget) return res.status(404).json({ error: 'Widget not found' });
|
||||||
|
if (widget.workspace_id && widget.workspace_id !== req.playlist.workspace_id) {
|
||||||
|
return res.status(403).json({ error: 'Widget is not in this playlist\'s workspace' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updates.push('content_id = ?'); values.push(newContentId || null);
|
||||||
|
updates.push('widget_id = ?'); values.push(newWidgetId || null);
|
||||||
|
}
|
||||||
|
|
||||||
if (updates.length > 0) {
|
if (updates.length > 0) {
|
||||||
updates.push("updated_at = strftime('%s','now')");
|
updates.push("updated_at = strftime('%s','now')");
|
||||||
values.push(req.params.itemId);
|
values.push(req.params.itemId);
|
||||||
|
|
@ -490,6 +521,43 @@ router.delete('/:id/items/:itemId', requirePlaylistWrite, (req, res) => {
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #105 duplicate: append a copy of an item (same content/widget + zone + duration)
|
||||||
|
// plus its schedule rows (new ids). One transaction so a half-copied item can't exist.
|
||||||
|
router.post('/:id/items/:itemId/duplicate', requirePlaylistWrite, (req, res) => {
|
||||||
|
const item = db.prepare('SELECT * FROM playlist_items WHERE id = ? AND playlist_id = ?')
|
||||||
|
.get(req.params.itemId, req.params.id);
|
||||||
|
if (!item) return res.status(404).json({ error: 'item not found' });
|
||||||
|
|
||||||
|
const copy = db.transaction(() => {
|
||||||
|
const max = db.prepare('SELECT MAX(sort_order) as m FROM playlist_items WHERE playlist_id = ?').get(req.params.id);
|
||||||
|
const order = (max.m || 0) + 1;
|
||||||
|
const result = db.prepare(`
|
||||||
|
INSERT INTO playlist_items (playlist_id, content_id, widget_id, zone_id, sort_order, duration_sec)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(req.params.id, item.content_id, item.widget_id, item.zone_id, order, item.duration_sec);
|
||||||
|
const newId = result.lastInsertRowid;
|
||||||
|
const scheds = db.prepare('SELECT active_days, start_time, end_time, start_date, end_date, sort_order FROM playlist_item_schedules WHERE playlist_item_id = ?').all(req.params.itemId);
|
||||||
|
const insSched = db.prepare('INSERT INTO playlist_item_schedules (id, playlist_item_id, active_days, start_time, end_time, start_date, end_date, sort_order) VALUES (?,?,?,?,?,?,?,?)');
|
||||||
|
for (const s of scheds) insSched.run(uuidv4(), newId, s.active_days, s.start_time, s.end_time, s.start_date, s.end_date, s.sort_order);
|
||||||
|
return newId;
|
||||||
|
});
|
||||||
|
const newId = copy();
|
||||||
|
markDraft(req.params.id);
|
||||||
|
|
||||||
|
const newItem = db.prepare(`
|
||||||
|
SELECT pi.*,
|
||||||
|
COALESCE(c.filename, w.name) as filename,
|
||||||
|
c.mime_type, c.filepath, c.thumbnail_path,
|
||||||
|
c.duration_sec as content_duration, c.file_size, c.remote_url,
|
||||||
|
w.name as widget_name, w.widget_type, w.config as widget_config
|
||||||
|
FROM playlist_items pi
|
||||||
|
LEFT JOIN content c ON pi.content_id = c.id
|
||||||
|
LEFT JOIN widgets w ON pi.widget_id = w.id
|
||||||
|
WHERE pi.id = ?
|
||||||
|
`).get(newId);
|
||||||
|
res.status(201).json(newItem);
|
||||||
|
});
|
||||||
|
|
||||||
// Reorder items
|
// Reorder items
|
||||||
router.post('/:id/items/reorder', requirePlaylistWrite, (req, res) => {
|
router.post('/:id/items/reorder', requirePlaylistWrite, (req, res) => {
|
||||||
const { order } = req.body;
|
const { order } = req.body;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue