From f5b158507b14bd2f69c2ab7e816c6eb08fce925d Mon Sep 17 00:00:00 2001 From: Martastain Date: Thu, 12 Dec 2024 22:49:15 +0100 Subject: [PATCH 1/2] fix: inheritance in scheduler and rundown --- frontend/src/containers/Calendar/Calendar.jsx | 13 +-- .../src/pages/AssetEditor/AssetEditor.jsx | 9 +- frontend/src/pages/Rundown/RundownTable.jsx | 109 +++++++++++++++--- frontend/src/pages/Rundown/utils.js | 8 +- .../Scheduler/ApplySchedulingTemplate.jsx | 1 - frontend/src/pages/Scheduler/Scheduler.jsx | 69 +++++++++-- .../src/tableFormat/formatObjectTitle.jsx | 4 +- .../src/tableFormat/formatRundownSymbol.jsx | 6 +- 8 files changed, 173 insertions(+), 46 deletions(-) diff --git a/frontend/src/containers/Calendar/Calendar.jsx b/frontend/src/containers/Calendar/Calendar.jsx index d8fb100..9f33dbd 100644 --- a/frontend/src/containers/Calendar/Calendar.jsx +++ b/frontend/src/containers/Calendar/Calendar.jsx @@ -21,6 +21,7 @@ const Calendar = ({ draggedAsset, events, saveEvent, + copyEvent, contextMenu, }) => { const navigate = useNavigate() @@ -290,16 +291,8 @@ const Calendar = ({ // handle control key for copying events if (e.ctrlKey) { console.log('Copying event', draggedEvent.current) - saveEvent({ - id: null, - is_empty_event: true, - start: Math.floor(cursorTime.current.getTime() / 1000), - // TODO: copy metadata based on channel config - title: draggedEvent.current.title, - subtitle: draggedEvent.current.subtitle, - duration: draggedEvent.current.duration, - color: draggedEvent.current.color, - }) + const newTs = Math.floor(cursorTime.current.getTime() / 1000) + copyEvent(draggedEvent.current.id, newTs) } else { saveEvent({ id: draggedEvent.current.id, diff --git a/frontend/src/pages/AssetEditor/AssetEditor.jsx b/frontend/src/pages/AssetEditor/AssetEditor.jsx index a4cdc91..1fcb897 100644 --- a/frontend/src/pages/AssetEditor/AssetEditor.jsx +++ b/frontend/src/pages/AssetEditor/AssetEditor.jsx @@ -1,6 +1,6 @@ import nebula from '/src/nebula' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useSearchParams } from 'react-router-dom' import { useEffect, useState, useMemo } from 'react' import { useSelector, useDispatch } from 'react-redux' import { toast } from 'react-toastify' @@ -72,6 +72,7 @@ const AssetEditor = () => { const [originalData, setOriginalData] = useState({}) const [loading, setLoading] = useState(false) const [editorMode, setEditorMode] = useLocalStorage('editorMode', 'metadata') + const [searchParams, setSearchParams] = useSearchParams() const showDialog = useDialog() @@ -84,7 +85,11 @@ const AssetEditor = () => { .then((response) => { setAssetData(response.data.data[0] || {}) setOriginalData(response.data.data[0] || {}) - navigate({ pathname: `/mam/editor`, search: `?asset=${id_asset}` }) + //navigate({ pathname: `/mam/editor`, search: `?asset=${id_asset}` }) + setSearchParams((o) => { + o.set('asset', id_asset) + return o + }) }) .catch((error) => { toast.error( diff --git a/frontend/src/pages/Rundown/RundownTable.jsx b/frontend/src/pages/Rundown/RundownTable.jsx index 049919a..a5b1282 100644 --- a/frontend/src/pages/Rundown/RundownTable.jsx +++ b/frontend/src/pages/Rundown/RundownTable.jsx @@ -106,6 +106,39 @@ const RundownTable = ({ .catch(() => {}) } + const onSetPrimary = async () => { + const id_asset = focusedObject.id_asset + const id_event = focusedObject.id_event + try { + const res = await nebula.request('get', { + object_type: 'asset', + ids: [id_asset], + }) + console.log('Asset:', res.data.data) + if (!res.data?.data?.length) { + console.error('Asset not found:', id_asset) + return + } + const meta = res.data.data[0] + const emeta = {} + for (const field of channelConfig.fields) { + const key = field.name + emeta[key] = meta[key] || null + } + emeta.id_asset = id_asset + await nebula.request('set', { + object_type: 'event', + id: id_event, + data: emeta, + }) + + loadRundown() + } catch (err) { + onError(err) + return + } + } + const onSolve = (solver) => { const items = data .filter( @@ -126,17 +159,28 @@ const RundownTable = ({ updateObject(object_type, id, { run_mode }) } - const editObject = (object_type, id) => { - const objectData = data.find( - (row) => row.id === id && row.type === object_type - ) - if (!objectData) return // this should never happen + const editObject = async (object_type, id) => { + let objectData = {} + try { + const res = await nebula.request('get', { + object_type, + ids: [id], + }) + objectData = res.data.data[0] + } catch (err) { + onError(err) + return + } + + // Create a field list based on the object type let fields - if (objectData.type === 'event') { + if (object_type === 'event') { fields = [...channelConfig.fields] } else if (objectData.item_role === 'placeholder') { fields = [{ name: 'title' }, { name: 'duration' }] + } else if (['lead_in', 'lead_out'].includes(objectData.item_role)) { + return } else if (objectData.id_asset) { fields = [ { name: 'title' }, @@ -145,24 +189,44 @@ const RundownTable = ({ { name: 'mark_in' }, { name: 'mark_out' }, ] - } else if (['lead_in', 'lead_out'].includes(objectData.item_role)) { - return } else { return } - const title = `Edit ${objectData.type}: ${objectData.title}` + // if the object is item with asset, we need to get the asset data + if (object_type === 'item' && objectData.id_asset) { + try { + const res = await nebula.request('get', { + object_type: 'asset', + ids: [objectData.id_asset], + }) + const assetData = res.data.data[0] + for (const field of fields) { + const key = field.name + if (!objectData.key) objectData[key] = assetData[key] + } + } catch (err) { + onError(err) + return + } + } + + // construct the form title and initial data + + const title = `Edit ${object_type}: ${objectData.title}` const initialData = {} for (const field of fields) { initialData[field.name] = objectData[field.name] } - showDialog('metadata', title, { fields, initialData }) - .then((newData) => { - updateObject(objectData.type, objectData.id, newData) + try { + const newData = await showDialog('metadata', title, { + fields, + initialData, }) - .catch(() => {}) + updateObject(object_type, id, newData) + } catch {} } // @@ -263,21 +327,26 @@ const RundownTable = ({ const res = [] if (selectedItems.length) { if (selectedItems.length === 1) { - res.push(...getRunModeOptions('item', selectedItems[0], setRunMode)) res.push({ label: 'Edit item', icon: 'edit', - separator: true, onClick: () => editObject('item', selectedItems[0]), }) - } - if (focusedObject.id_asset) { + if (focusedObject.id_asset) { + res.push({ + label: 'Set as primary', + icon: 'star', + onClick: onSetPrimary, + }) + } + res.push({ label: 'Send to...', icon: 'send', onClick: onSendTo, }) } + if (focusedObject.item_role === 'placeholder') { for (const solver of channelConfig.solvers) { res.push({ @@ -287,6 +356,9 @@ const RundownTable = ({ }) } } + + res.push(...getRunModeOptions('item', selectedItems[0], setRunMode)) + res.push({ label: 'Delete', icon: 'delete', @@ -296,13 +368,12 @@ const RundownTable = ({ }) return res } else if (selectedEvents.length === 1) { - res.push(...getRunModeOptions('event', selectedEvents[0], setRunMode)) res.push({ label: 'Edit event', icon: 'edit', - separator: true, onClick: () => editObject('event', selectedEvents[0]), }) + res.push(...getRunModeOptions('event', selectedEvents[0], setRunMode)) } return res } diff --git a/frontend/src/pages/Rundown/utils.js b/frontend/src/pages/Rundown/utils.js index 5eaecfd..aa05f3b 100644 --- a/frontend/src/pages/Rundown/utils.js +++ b/frontend/src/pages/Rundown/utils.js @@ -7,6 +7,7 @@ const getRunModeOptions = (object_type, selection, func) => { { label: 'Run: Auto', icon: 'play_arrow', + separator: true, onClick: () => func('event', selection, 0), }, { @@ -29,17 +30,18 @@ const getRunModeOptions = (object_type, selection, func) => { if (object_type === 'item') { return [ { - label: 'Run: Auto', + label: 'Run auto', icon: 'play_arrow', + separator: true, onClick: () => func('item', selection, 0), }, { - label: 'Run: Manual', + label: 'Manual', icon: 'hand_gesture', onClick: () => func('item', selection, 1), }, { - label: 'Run: Skip', + label: 'Skip', icon: 'skip_next', onClick: () => func('item', selection, 4), }, diff --git a/frontend/src/pages/Scheduler/ApplySchedulingTemplate.jsx b/frontend/src/pages/Scheduler/ApplySchedulingTemplate.jsx index 68e5452..e8cbc37 100644 --- a/frontend/src/pages/Scheduler/ApplySchedulingTemplate.jsx +++ b/frontend/src/pages/Scheduler/ApplySchedulingTemplate.jsx @@ -29,7 +29,6 @@ const ApplySchedulingTemplate = ({ loadEvents, date, loading, setLoading }) => { if (b.name === channelConfig.default_template) return 1 return a.title.localeCompare(b.title) }) - console.log(templates) setTemplates(templates) }) } diff --git a/frontend/src/pages/Scheduler/Scheduler.jsx b/frontend/src/pages/Scheduler/Scheduler.jsx index 11ad68e..ec91f6d 100644 --- a/frontend/src/pages/Scheduler/Scheduler.jsx +++ b/frontend/src/pages/Scheduler/Scheduler.jsx @@ -69,6 +69,41 @@ const Scheduler = ({ draggedObjects }) => { // Saving events to the server + const copyEvent = async (id, newTs) => { + const fields = [{ name: 'start' }, ...channelConfig.fields] + const initialData = {} + const finalData = {} + + try { + const res = await nebula.request('get', { + object_type: 'event', + ids: [id], + }) + const edata = res.data.data[0] + for (const field of fields) { + initialData[field.name] = edata[field.name] + } + finalData.id_asset = edata.id_asset + } catch (e) { + console.error('Unable to load event', e) + return + } + initialData.start = newTs + + try { + const title = `Copy event: ${initialData.title || 'Untitled'}` + const res = await showDialog('metadata', title, { fields, initialData }) + console.log('res', res) + for (const field of fields) { + finalData[field.name] = res[field.name] || null + } + } catch (e) { + // + } + console.log('finalData', finalData) + saveEvent(finalData) + } + const saveEvent = async (event) => { const payload = { start: event.start, @@ -128,17 +163,32 @@ const Scheduler = ({ draggedObjects }) => { // Context menu // - const editEvent = (event) => { + const editEvent = async (event) => { const title = `Edit event: ${event.title || 'Untitled'}` const fields = [{ name: 'start' }, ...channelConfig.fields] - showDialog('metadata', title, { fields, initialData: event }) - .then((data) => { - console.log('Saving', data) - saveEvent({ ...data }) - }) - .catch(() => { - console.log('Cancelled') - }) + + const initialData = {} + if (event.id) { + try { + const res = await nebula.request('get', { + object_type: 'event', + ids: [event.id], + }) + const edata = res.data.data[0] + for (const field of fields) { + initialData[field.name] = edata[field.name] + } + } catch (e) { + console.error('Failed to load event', e) + } + } + + try { + const r = await showDialog('metadata', title, { fields, initialData }) + saveEvent({ ...r, id: event.id }) + } catch (e) { + // + } } const deleteEvent = (eventId) => { @@ -201,6 +251,7 @@ const Scheduler = ({ draggedObjects }) => { startTime={startTime} events={events} saveEvent={saveEvent} + copyEvent={copyEvent} draggedAsset={draggedAsset} contextMenu={contextMenu} /> diff --git a/frontend/src/tableFormat/formatObjectTitle.jsx b/frontend/src/tableFormat/formatObjectTitle.jsx index 6ed4de5..422f56c 100644 --- a/frontend/src/tableFormat/formatObjectTitle.jsx +++ b/frontend/src/tableFormat/formatObjectTitle.jsx @@ -12,10 +12,12 @@ const formatObjectTitle = (rowData, key) => { const title = rowData[key] const subtitle = rowData.subtitle const note = rowData.note + const tstyle = {} + if (rowData.is_primary) tstyle.fontWeight = 'bold' return (
- {title} + {title} {subtitle && ( {nebula.settings.system.subtitle_separator} diff --git a/frontend/src/tableFormat/formatRundownSymbol.jsx b/frontend/src/tableFormat/formatRundownSymbol.jsx index 5240ca0..1396b89 100644 --- a/frontend/src/tableFormat/formatRundownSymbol.jsx +++ b/frontend/src/tableFormat/formatRundownSymbol.jsx @@ -6,13 +6,17 @@ const formatRundownSymbol = (rowData) => { } const style = {} + //style.fontSize = '18px' let icon = '' if (rowData.id_asset) { const folder = nebula.settings.folders.find( (f) => f.id === rowData.id_folder ) style.color = folder?.color - icon = 'fiber_manual_record' + // icon = rowData.is_primary + // ? 'radio_button_checked' + // : 'radio_button_unchecked' + icon = rowData.is_primary ? 'star' : 'fiber_manual_record' } else if (rowData.item_role === 'placeholder') { icon = 'expand' } else if (rowData.item_role === 'live') { From c854706c0e21ce53998d039314a70d6b58c4384b Mon Sep 17 00:00:00 2001 From: Martastain Date: Sat, 14 Dec 2024 11:02:15 +0100 Subject: [PATCH 2/2] fix: update poster frame mark when it changes --- .../src/containers/VideoPlayer/Trackbar.jsx | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/frontend/src/containers/VideoPlayer/Trackbar.jsx b/frontend/src/containers/VideoPlayer/Trackbar.jsx index f0c532b..e78bd90 100644 --- a/frontend/src/containers/VideoPlayer/Trackbar.jsx +++ b/frontend/src/containers/VideoPlayer/Trackbar.jsx @@ -90,19 +90,6 @@ const Trackbar = ({ ctx.lineTo(markOutX, height - 1) ctx.stroke() - // Draw the poster frame - - if (auxMarks.poster_frame) { - const posterFrameX = (auxMarks.poster_frame / duration) * width - ctx.fillStyle = '#ff00ff' - ctx.beginPath() - ctx.moveTo(posterFrameX - 4, 0) - ctx.lineTo(posterFrameX + 4, 0) - ctx.lineTo(posterFrameX, 4) - ctx.closePath() - ctx.fill() - } - // // Draw the handle // @@ -124,13 +111,27 @@ const Trackbar = ({ ctx.beginPath() ctx.fillRect(progressX - 1, 0, handleWidth, height) ctx.fill() - }, [currentTime, duration, markIn, markOut]) + + // Draw the poster frame + + if (auxMarks.poster_frame) { + const posterFrameX = + (auxMarks.poster_frame / duration) * width + frameWidth / 2 + ctx.fillStyle = '#ff00ff' + ctx.beginPath() + ctx.moveTo(posterFrameX - 4, 0) + ctx.lineTo(posterFrameX + 4, 0) + ctx.lineTo(posterFrameX, 4) + ctx.closePath() + ctx.fill() + } + }, [currentTime, duration, markIn, markOut, marks]) // Events useEffect(() => { drawSlider() - }, [currentTime, duration, markIn, markOut]) + }, [currentTime, duration, markIn, markOut, marks]) // Dragging