From 423ea66b15525cc78fe3fc5d993b5902f73f9ca0 Mon Sep 17 00:00:00 2001 From: Simon Mellberg Date: Fri, 20 Mar 2026 23:24:34 +0100 Subject: [PATCH 1/6] Ready for review Signed-off-by: Simon Mellberg --- plugins/caspar/app/App.jsx | 3 + .../app/components/LibraryListItem/index.jsx | 4 +- .../caspar/app/components/MediaSeek/index.jsx | 168 ++++++++++++++++++ .../caspar/app/components/MediaSeek/style.css | 87 +++++++++ plugins/caspar/app/utils/asset.cjs | 29 +++ plugins/caspar/app/utils/asset.unit.test.js | 41 ++++- plugins/caspar/app/views/InspectorRange.jsx | 80 +++++++++ plugins/caspar/lib/handlers.js | 4 +- plugins/caspar/lib/types.js | 15 ++ 9 files changed, 427 insertions(+), 4 deletions(-) create mode 100644 plugins/caspar/app/components/MediaSeek/index.jsx create mode 100644 plugins/caspar/app/components/MediaSeek/style.css create mode 100644 plugins/caspar/app/views/InspectorRange.jsx diff --git a/plugins/caspar/app/App.jsx b/plugins/caspar/app/App.jsx index 4a19ac60..ccc4216f 100644 --- a/plugins/caspar/app/App.jsx +++ b/plugins/caspar/app/App.jsx @@ -3,6 +3,7 @@ import React from 'react' import * as SharedContext from './sharedContext' import * as Settings from './views/Settings' +import { InspectorRange } from './views/InspectorRange' import { InspectorServer } from './views/InspectorServer' import { InspectorTemplate } from './views/InspectorTemplate' import { InspectorTransition } from './views/InspectorTransition' @@ -31,6 +32,8 @@ export default function App () { return case 'inspector/template': return + case 'inspector/range': + return case 'settings/servers': return case 'liveSwitch': diff --git a/plugins/caspar/app/components/LibraryListItem/index.jsx b/plugins/caspar/app/components/LibraryListItem/index.jsx index 417fedf9..881f7354 100644 --- a/plugins/caspar/app/components/LibraryListItem/index.jsx +++ b/plugins/caspar/app/components/LibraryListItem/index.jsx @@ -43,7 +43,9 @@ const ITEM_CONSTRUCTORS = [ target: item.name, ...(DEFAULT_VALUES[item.type] || {}) }, - duration: asset.calculateDurationMs(item) + duration: asset.calculateDurationMs(item), + medialength: Number.parseInt(item?.duration), + framerate: asset.frameRateFractionToDecimal(item?.framerate) } } } diff --git a/plugins/caspar/app/components/MediaSeek/index.jsx b/plugins/caspar/app/components/MediaSeek/index.jsx new file mode 100644 index 00000000..86f681da --- /dev/null +++ b/plugins/caspar/app/components/MediaSeek/index.jsx @@ -0,0 +1,168 @@ +import React, { useRef, useEffect, useState } from 'react' +import { millisecondsToTime } from '../../utils/asset.cjs' +import "./style.css" + +const MediaHandle = ({ style, handlePointerDown, type, timestamp }) => { + return ( +
handlePointerDown(type)} + > + {timestamp} +
+ ) +} + +const MediaSelectionBar = ({ + handlePositions, + handlePointerDown, + maxValue, +}) => { + const [lastActiveHandle, setLastActiveHandle] = useState(null) + + const pointerDown = (type) => { + handlePointerDown(type) + setLastActiveHandle(type) + } + + const isFullRange = + handlePositions.in === 0 && handlePositions.out === maxValue + const leftHandlePos = (handlePositions.in / maxValue) * 100 + const rightHandlePos = (handlePositions.out / maxValue) * 100 + return ( + <> +
+
+
handlePointerDown("bar")} + style={{ + opacity: isFullRange ? 0 : 1, + + pointerEvents: isFullRange ? "none" : "auto", + }} + > + = +
+
+
+ +
+ + + +
+ + ) +} + +export const MediaSeek = ({ inValue, outValue, maxValue, onChange }) => { + const trackRef = useRef(null) + const [dragging, setDragging] = useState(null) + const [handlePositions, setHandlePositions] = useState({ + in: inValue, + out: outValue, + }) + + useEffect(() => { + setHandlePositions({ in: inValue, out: outValue }) + }, [inValue, outValue]) + + const handlePointerMove = (e) => { + const rect = trackRef.current.getBoundingClientRect() + const percent = (e.clientX - rect.left) / rect.width + const clampedPercent = Math.max(0, Math.min(1, percent)) + const value = clampedPercent * maxValue + + if (dragging === "in") { + setHandlePositions((prev) => ({ + ...prev, + in: Math.min(value, prev.out), + })) + } else if (dragging === "out") { + setHandlePositions((prev) => ({ + ...prev, + out: Math.max(value, prev.in), + })) + } else { // dragging the whole bar + const range = handlePositions.out - handlePositions.in + let newInPoint = value - range / 2 + let newOutPoint = value + range / 2 + + // Don't allow the bar to be dragged out of bounds + if (newInPoint <= 0) { + newInPoint = 0 + newOutPoint = range + } else if (newOutPoint >= maxValue) { + newOutPoint = maxValue + newInPoint = maxValue - range + } + + setHandlePositions(() => ({ + in: newInPoint, + out: newOutPoint, + })) + } + } + + const handlePointerDown = (type) => { + setDragging(type) + } + + const handlePointerUp = () => { + // Only trigger onChange if values have changed + if (handlePositions.in !== inValue || handlePositions.out !== outValue) { + onChange(handlePositions.in, handlePositions.out) + } + setDragging(null) + } + + useEffect(() => { + if (dragging) { + window.addEventListener("pointermove", handlePointerMove) + window.addEventListener("pointerup", handlePointerUp) + } + + return () => { + window.removeEventListener("pointermove", handlePointerMove) + window.removeEventListener("pointerup", handlePointerUp) + } + }, [dragging, handlePointerMove, handlePointerUp]) + + return ( +
+
+ +
+
+ ) +} diff --git a/plugins/caspar/app/components/MediaSeek/style.css b/plugins/caspar/app/components/MediaSeek/style.css new file mode 100644 index 00000000..a87fc22b --- /dev/null +++ b/plugins/caspar/app/components/MediaSeek/style.css @@ -0,0 +1,87 @@ +.MediaSeekBar { + width: 100%; + margin: 0; + user-select: none; + font-family: sans-serif; + border-radius: 0.6rem; +} + +.MediaSeekBar-track { + position: relative; + height: 34px; + background-color: var(--base-color--shade2); + border-radius: inherit; + cursor: initial; + box-shadow: inset 0 0 0 1px var(--base-color--shade2); + overflow: visible; +} + +.MediaSelectionBar { + position: relative; + height: inherit; + border-radius: inherit; + overflow: hidden; + cursor: initial; +} + +.MediaSelectionBar-highlighted { + position: absolute; + top: 0; + bottom: 0; + background-color: var(--base-color--shade4); + opacity: 0.5; + border-radius: 0.25rem; +} + +.MediaSelectionBar-highlightedBar { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; + font-size: 100%; + color: var(--base-color--shade3); + user-select: none; + cursor: grab; + padding: 0.1rem 0.5rem; +} + +.MediaSelectionBar-highlightedBar:active { + cursor: grabbing; +} + +.MediaHandle { + position: absolute; + top: 15%; + width: 10px; + height: 70%; + background-color: var(--base-color); + border-radius: 0.25rem; + cursor: ew-resize; + transform: translateX(-50%); +} + +.MediaHandle--Value { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + margin-top: 4px; + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + white-space: nowrap; + z-index: 1; +} + +.MediaHandle::after { + content: ""; + position: absolute; + top: 15%; + height: 70%; + left: 50%; + transform: translateX(-50%); + width: 20%; + border-radius: inherit; + background-color: black; +} diff --git a/plugins/caspar/app/utils/asset.cjs b/plugins/caspar/app/utils/asset.cjs index c6e180b8..87b40ffa 100644 --- a/plugins/caspar/app/utils/asset.cjs +++ b/plugins/caspar/app/utils/asset.cjs @@ -114,3 +114,32 @@ function frameRateFractionToDecimal (fraction) { return dividend / divisor } +exports.frameRateFractionToDecimal = frameRateFractionToDecimal + +/** + * Build a string in hours:minutes:seconds format from milliseconds + * @example + * 1000 -> '00:01' + * 61000 -> '01:01' + * 3600000 -> '01:00:00' + * @param {number} ms + * @returns {string} + */ +function millisecondsToTime(ms) { + if (ms < 0) return "00:00" + if (!ms) return "00:00" + if (isNaN(ms)) return "00:00" + + const totalSeconds = Math.floor(ms / 1000) + const hours = Math.floor(totalSeconds / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const seconds = totalSeconds % 60 + const pad = (n) => n.toString().padStart(2, "0") + + if (hours > 0) { + return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}` + } else { + return `${pad(minutes)}:${pad(seconds)}` + } +} +exports.millisecondsToTime = millisecondsToTime \ No newline at end of file diff --git a/plugins/caspar/app/utils/asset.unit.test.js b/plugins/caspar/app/utils/asset.unit.test.js index 80536f3e..6a1caca9 100644 --- a/plugins/caspar/app/utils/asset.unit.test.js +++ b/plugins/caspar/app/utils/asset.unit.test.js @@ -1,4 +1,4 @@ -const { calculateDurationMs } = require('./asset.cjs') +const { calculateDurationMs, millisecondsToTime } = require('./asset.cjs') test('calculateDurationMs should return 0 for 0 duration (string or number)', () => { // Test for both string and number '0' @@ -53,3 +53,42 @@ test('calculateDurationMs should return 0 for STILL image', () => { } expect(calculateDurationMs(item)).toEqual(0) }) + +test('calculateDurationMs should return default time for non-numeric duration', () => { + const item = { + framerate: '1/25', + duration: 'not-a-number' + } + expect(calculateDurationMs(item)).toEqual(5000) +}) + +test('returns "00:00" for 0 or negative input', () => { + expect(millisecondsToTime(0)).toEqual('00:00') + expect(millisecondsToTime(-100)).toEqual('00:00') + expect(millisecondsToTime(null)).toEqual('00:00') + expect(millisecondsToTime(undefined)).toEqual('00:00') +}) + +test('converts seconds correctly', () => { + expect(millisecondsToTime(1000)).toEqual('00:01') + expect(millisecondsToTime(61000)).toEqual('01:01') +}) + +test('converts hours correctly', () => { + expect(millisecondsToTime(3600000)).toEqual('01:00:00') + expect(millisecondsToTime(3661000)).toEqual('01:01:01') +}) + +test('handles arbitrary times', () => { + expect(millisecondsToTime(7325000)).toEqual('02:02:05') +}) + +test('handles non-numeric input gracefully', () => { + expect(millisecondsToTime('not-a-number')).toEqual('00:00') + expect(millisecondsToTime({})).toEqual('00:00') + expect(millisecondsToTime([])).toEqual('00:00') +}) + +test('handles large durations', () => { + expect(millisecondsToTime(100 * 3600000)).toEqual('100:00:00') +}) diff --git a/plugins/caspar/app/views/InspectorRange.jsx b/plugins/caspar/app/views/InspectorRange.jsx new file mode 100644 index 00000000..3cea85f5 --- /dev/null +++ b/plugins/caspar/app/views/InspectorRange.jsx @@ -0,0 +1,80 @@ +import React from 'react' +import bridge from 'bridge' +import { SharedContext } from '../sharedContext' +import { MediaSeek } from '../components/MediaSeek' + +export const InspectorRange = () => { + const [state] = React.useContext(SharedContext) + const [selection, setSelection] = React.useState([]) + + // Fetch selection when state changes + React.useEffect(() => { + async function updateSelection() { + const sel = await bridge.client.selection.getSelection() + setSelection(sel) + } + updateSelection() + }, [state, state?.items[0]?.data?.caspar?.seek, state?.items[0]?.data?.duration]) + + + const items = selection.map(id => state?.items?.[id]) + const item = items[0] + if (!item) return null + + // Only allow seek for one item at a time + if (items.length > 1) { + return ( +
+

Multiple items selected

+
+ ) + } + + // Get the seek and length and total frames from the first selected item + const seek = Number.parseInt(item?.data?.caspar?.seek) || 0 + const medialength = item?.data?.medialength + const length = item?.data?.caspar?.length === undefined + ? (medialength - seek) + : item?.data?.caspar?.length + + const framerate = item?.data?.framerate || 25 + + // Convert frames to milliseconds + const inValue = seek / framerate * 1000 + const outValue = (length+seek) / framerate * 1000 + const maxValue = medialength / framerate * 1000 + + function handleChange (newIn, newOut) { + for (const id of selection) { + + const item = state?.items?.[id] + const framerate = item?.data?.framerate || 25 + + // newIn and newOut are in milliseconds + const seekFrames = Math.round((newIn / 1000) * framerate) // frames + const lengthFrames = Math.round(((newOut - newIn) / 1000) * framerate) // frames + const duration = Math.round(newOut - newIn) // milliseconds + + bridge.items.applyItem(id, { + data: { + caspar: { + seek: seekFrames, + length: lengthFrames + }, + duration + } + }) + } + } + + return ( +
+ +
+ ) +} diff --git a/plugins/caspar/lib/handlers.js b/plugins/caspar/lib/handlers.js index 3b2955b0..f9c569dd 100644 --- a/plugins/caspar/lib/handlers.js +++ b/plugins/caspar/lib/handlers.js @@ -22,13 +22,13 @@ const PLAY_HANDLERS = { return commands.sendString(serverId, item?.data?.caspar?.amcp) }, 'bridge.caspar.media': async (serverId, item) => { - return commands.sendCommand(serverId, 'play', item?.data?.caspar?.target, item?.data?.caspar?.loop, 0, undefined, undefined, undefined, item?.data?.caspar) + return commands.sendCommand(serverId, 'play', item?.data?.caspar?.target, item?.data?.caspar?.loop, item?.data?.caspar?.seek, item?.data?.caspar?.length, undefined, undefined, item?.data?.caspar) }, 'bridge.caspar.image-scroller': async (serverId, item) => { return commands.sendCommand(serverId, 'playImageScroller', item?.data?.caspar?.target, item?.data?.caspar) }, 'bridge.caspar.load': async (serverId, item) => { - return commands.sendCommand(serverId, 'loadbg', item?.data?.caspar?.target, item?.data?.caspar?.loop, 0, undefined, undefined, item?.data?.caspar?.auto, item?.data?.caspar) + return commands.sendCommand(serverId, 'loadbg', item?.data?.caspar?.target, item?.data?.caspar?.loop, item?.data?.caspar?.seek, item?.data?.caspar?.length, undefined, item?.data?.caspar?.auto, item?.data?.caspar) }, 'bridge.caspar.template': (serverId, item) => { return commands.sendCommand(serverId, 'cgAdd', item?.data?.caspar?.target, getCleanTemplateDataString(item), true, item?.data?.caspar) diff --git a/plugins/caspar/lib/types.js b/plugins/caspar/lib/types.js index d9dc0871..9c4b348a 100644 --- a/plugins/caspar/lib/types.js +++ b/plugins/caspar/lib/types.js @@ -189,6 +189,21 @@ function init (htmlPath) { type: 'string', 'ui.group': 'Transition', 'ui.uri': `${htmlPath}?path=inspector/transition` + }, + 'caspar.seek': { + name: 'Seek', + type: 'string', + default: '0', + allowsVariables: true, + 'ui.group': 'Timing', + 'ui.unit': 'frames' + }, + 'caspar.range': { + name: '', + type: 'string', + allowsVariables: false, + 'ui.group': 'Timing', + 'ui.uri': `${htmlPath}?path=inspector/range` } } }) From a486fe386e636c96810b783e83d4fe7bcd64f2db Mon Sep 17 00:00:00 2001 From: Simon Mellberg Date: Fri, 20 Mar 2026 23:37:40 +0100 Subject: [PATCH 2/6] Don't show seek bar for images with length 0 Signed-off-by: Simon Mellberg --- plugins/caspar/app/views/InspectorRange.jsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/caspar/app/views/InspectorRange.jsx b/plugins/caspar/app/views/InspectorRange.jsx index 3cea85f5..01b54551 100644 --- a/plugins/caspar/app/views/InspectorRange.jsx +++ b/plugins/caspar/app/views/InspectorRange.jsx @@ -30,12 +30,15 @@ export const InspectorRange = () => { ) } - // Get the seek and length and total frames from the first selected item - const seek = Number.parseInt(item?.data?.caspar?.seek) || 0 + // Get parameters from the first selected item const medialength = item?.data?.medialength + // Don't show seek bar if length is 0 + if (medialength === 0) return null + + const seek = Number.parseInt(item?.data?.caspar?.seek) || 0 const length = item?.data?.caspar?.length === undefined ? (medialength - seek) - : item?.data?.caspar?.length + : item?.data?.caspar?.length const framerate = item?.data?.framerate || 25 From b6cb795349560ff43b27f9d9efa344a4b3492054 Mon Sep 17 00:00:00 2001 From: Simon Mellberg Date: Fri, 20 Mar 2026 23:43:00 +0100 Subject: [PATCH 3/6] Removed uneccessary comments Signed-off-by: Simon Mellberg --- plugins/caspar/app/utils/asset.cjs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/caspar/app/utils/asset.cjs b/plugins/caspar/app/utils/asset.cjs index 87b40ffa..0d311b68 100644 --- a/plugins/caspar/app/utils/asset.cjs +++ b/plugins/caspar/app/utils/asset.cjs @@ -62,16 +62,15 @@ function calculateDurationMs (item) { if (!item) { return DEFAULT_DURATION_MS } - // If the item is a still image, return 0 + if (item.type === 'STILL') { return 0 } - // If the item has no duration, return the default duration + if (isNaN(Number(item?.duration))) { return DEFAULT_DURATION_MS } - // If the item has no framerate, return the default duration if (!item?.framerate) { return DEFAULT_DURATION_MS } From 44956a52c16fdbccfda4e4c11070e3c753d339df Mon Sep 17 00:00:00 2001 From: Simon Mellberg Date: Sat, 21 Mar 2026 00:04:49 +0100 Subject: [PATCH 4/6] Signed Signed-off-by: Simon Mellberg --- plugins/caspar/app/components/MediaSeek/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/caspar/app/components/MediaSeek/index.jsx b/plugins/caspar/app/components/MediaSeek/index.jsx index 86f681da..5feff57c 100644 --- a/plugins/caspar/app/components/MediaSeek/index.jsx +++ b/plugins/caspar/app/components/MediaSeek/index.jsx @@ -66,7 +66,6 @@ const MediaSelectionBar = ({ type="in" timestamp={millisecondsToTime(handlePositions.in)} /> - Date: Sat, 21 Mar 2026 01:25:41 +0100 Subject: [PATCH 5/6] Moved functions for converting frames to and from milliseconds to asset.cjs Signed-off-by: Simon Mellberg --- plugins/caspar/app/utils/asset.cjs | 39 +++++++++++++++- plugins/caspar/app/utils/asset.unit.test.js | 40 +++++++++++++++- plugins/caspar/app/views/InspectorRange.jsx | 52 ++++++++++----------- 3 files changed, 101 insertions(+), 30 deletions(-) diff --git a/plugins/caspar/app/utils/asset.cjs b/plugins/caspar/app/utils/asset.cjs index 0d311b68..e3ac3e06 100644 --- a/plugins/caspar/app/utils/asset.cjs +++ b/plugins/caspar/app/utils/asset.cjs @@ -141,4 +141,41 @@ function millisecondsToTime(ms) { return `${pad(minutes)}:${pad(seconds)}` } } -exports.millisecondsToTime = millisecondsToTime \ No newline at end of file +exports.millisecondsToTime = millisecondsToTime + +/** + * Frames to milliseconds + * @param {number} frames + * @param {number} framerate + * @returns {number} + */ +function framesToMilliseconds(frames, framerate) { + if (frames < 0) return 0 + if (!frames) return 0 + if (isNaN(frames)) return 0 + if (framerate <= 0) return 0 + if (!framerate) return 0 + if (isNaN(framerate)) return 0 + + // Round to 1 decimal place + return Math.round((frames / framerate) * 1000 * 10) / 10 +} +exports.framesToMilliseconds = framesToMilliseconds + +/** + * Milliseconds to frames + * @param {number} ms + * @param {number} framerate + * @returns {number} + */ +function millisecondsToFrames(ms, framerate) { + if (ms < 0) return 0 + if (!ms) return 0 + if (isNaN(ms)) return 0 + if (framerate <= 0) return 0 + if (!framerate) return 0 + if (isNaN(framerate)) return 0 + + return Math.round((ms / 1000) * framerate) +} +exports.millisecondsToFrames = millisecondsToFrames diff --git a/plugins/caspar/app/utils/asset.unit.test.js b/plugins/caspar/app/utils/asset.unit.test.js index 6a1caca9..d24cc04f 100644 --- a/plugins/caspar/app/utils/asset.unit.test.js +++ b/plugins/caspar/app/utils/asset.unit.test.js @@ -1,4 +1,4 @@ -const { calculateDurationMs, millisecondsToTime } = require('./asset.cjs') +const { calculateDurationMs, millisecondsToTime, framesToMilliseconds, millisecondsToFrames} = require('./asset.cjs') test('calculateDurationMs should return 0 for 0 duration (string or number)', () => { // Test for both string and number '0' @@ -92,3 +92,41 @@ test('handles non-numeric input gracefully', () => { test('handles large durations', () => { expect(millisecondsToTime(100 * 3600000)).toEqual('100:00:00') }) + +test('framesToMilliseconds converts frames to milliseconds correctly', () => { + expect(framesToMilliseconds(25, 25)).toEqual(1000) + expect(framesToMilliseconds(50, 25)).toEqual(2000) + expect(framesToMilliseconds(30, 29.97)).toEqual(1001.0) +}) + +test('framesToMilliseconds returns 0 for non-numeric frames', () => { + expect(framesToMilliseconds('not-a-number', 25)).toEqual(0) +}) + +test('framesToMilliseconds returns 0 for invalid framerate', () => { + expect(framesToMilliseconds(25, 'invalid')).toEqual(0) +}) + +test('framesToMilliseconds returns 0 for zero or negative framerate', () => { + expect(framesToMilliseconds(25, 0)).toEqual(0) + expect(framesToMilliseconds(25, -25)).toEqual(0) +}) + +test('millisecondsToFrames converts milliseconds to frames correctly', () => { + expect(millisecondsToFrames(1000, 25)).toEqual(25) + expect(millisecondsToFrames(2000, 25)).toEqual(50) + expect(millisecondsToFrames(1001, 29.97)).toBeCloseTo(30) +}) + +test('millisecondsToFrames returns 0 for non-numeric milliseconds', () => { + expect(millisecondsToFrames('not-a-number', 25)).toEqual(0) +}) + +test('millisecondsToFrames returns 0 for invalid framerate', () => { + expect(millisecondsToFrames(1000, 'invalid')).toEqual(0) +}) + +test('millisecondsToFrames returns 0 for zero or negative framerate', () => { + expect(millisecondsToFrames(1000, 0)).toEqual(0) + expect(millisecondsToFrames(1000, -25)).toEqual(0) +}) diff --git a/plugins/caspar/app/views/InspectorRange.jsx b/plugins/caspar/app/views/InspectorRange.jsx index 01b54551..94fdffea 100644 --- a/plugins/caspar/app/views/InspectorRange.jsx +++ b/plugins/caspar/app/views/InspectorRange.jsx @@ -2,6 +2,7 @@ import React from 'react' import bridge from 'bridge' import { SharedContext } from '../sharedContext' import { MediaSeek } from '../components/MediaSeek' +import { framesToMilliseconds, millisecondsToFrames} from '../utils/asset.cjs' export const InspectorRange = () => { const [state] = React.useContext(SharedContext) @@ -14,7 +15,7 @@ export const InspectorRange = () => { setSelection(sel) } updateSelection() - }, [state, state?.items[0]?.data?.caspar?.seek, state?.items[0]?.data?.duration]) + }, [state]) const items = selection.map(id => state?.items?.[id]) @@ -31,43 +32,38 @@ export const InspectorRange = () => { } // Get parameters from the first selected item - const medialength = item?.data?.medialength + const framerate = item?.data?.framerate || 25 + const medialength = item?.data?.medialength || millisecondsToFrames(item?.data?.duration, framerate) // Don't show seek bar if length is 0 if (medialength === 0) return null const seek = Number.parseInt(item?.data?.caspar?.seek) || 0 - const length = item?.data?.caspar?.length === undefined - ? (medialength - seek) - : item?.data?.caspar?.length - - const framerate = item?.data?.framerate || 25 + const length = item?.data?.caspar?.length || (medialength - seek) // Convert frames to milliseconds - const inValue = seek / framerate * 1000 - const outValue = (length+seek) / framerate * 1000 - const maxValue = medialength / framerate * 1000 + const inValue = framesToMilliseconds(seek, framerate) + const outValue = framesToMilliseconds(seek + length, framerate) + const maxValue = framesToMilliseconds(medialength, framerate) function handleChange (newIn, newOut) { - for (const id of selection) { + const id = selection[0] + const item = state?.items?.[id] + const framerate = item?.data?.framerate || 25 - const item = state?.items?.[id] - const framerate = item?.data?.framerate || 25 + // newIn and newOut are in milliseconds + const seekFrames = millisecondsToFrames(newIn, framerate) // frames + const lengthFrames = millisecondsToFrames(newOut - newIn, framerate) // frames + const duration = Math.round(newOut - newIn) // milliseconds - // newIn and newOut are in milliseconds - const seekFrames = Math.round((newIn / 1000) * framerate) // frames - const lengthFrames = Math.round(((newOut - newIn) / 1000) * framerate) // frames - const duration = Math.round(newOut - newIn) // milliseconds - - bridge.items.applyItem(id, { - data: { - caspar: { - seek: seekFrames, - length: lengthFrames - }, - duration - } - }) - } + bridge.items.applyItem(id, { + data: { + caspar: { + seek: seekFrames, + length: lengthFrames + }, + duration: duration + } + }) } return ( From 60789021ce5f842c0e99acc97d6b1ee975f3aca0 Mon Sep 17 00:00:00 2001 From: Simon Mellberg Date: Sat, 21 Mar 2026 09:25:33 +0100 Subject: [PATCH 6/6] WIP: Coloring for light theme Signed-off-by: Simon Mellberg --- plugins/caspar/app/components/MediaSeek/style.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/caspar/app/components/MediaSeek/style.css b/plugins/caspar/app/components/MediaSeek/style.css index a87fc22b..8f34d988 100644 --- a/plugins/caspar/app/components/MediaSeek/style.css +++ b/plugins/caspar/app/components/MediaSeek/style.css @@ -72,6 +72,8 @@ border-radius: 4px; white-space: nowrap; z-index: 1; + font-family: var(--base-fontFamily--primary); + color: var(--base-color); } .MediaHandle::after { @@ -83,5 +85,5 @@ transform: translateX(-50%); width: 20%; border-radius: inherit; - background-color: black; + background-color: invert(var(--base-color)); }