diff --git a/README.md b/README.md index a502cf57f..5884177d8 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/) - [x] Ability to differentiate between backstage and public data - [x] Manage delays workflow - [x] OSC Control and Feedback -- [x] Roll mode: run independently from system clock +- [x] Roll mode: run independently using the system clock ## Unopinionated We are not interested in forcing workflows and have made ontime so it is flexible to whichever way you would like to work. @@ -80,8 +80,6 @@ This will be receiving attention as we near v1 release - [ ] App appears visually broken: Please ensure that windows settings have no display zoom (it is 125% by default) - [ ] app needs improvement on handling zoomed interfaces - [ ] Very long titles might cause interface to shift -#### Roll -- [ ] Roll feature does not understand difference between public and backstage titles # Help Help is underway! ... and can be viewed [here](https://cpvalente.gitbook.io/ontime/) diff --git a/client/src/features/info/Info.jsx b/client/src/features/info/Info.jsx index e5dbb67c5..ad06d2b38 100644 --- a/client/src/features/info/Info.jsx +++ b/client/src/features/info/Info.jsx @@ -18,10 +18,9 @@ export default function Info() { noteNext: '', }); const [selected, setSelected] = useState('No events'); + const [playback, setPlayback] = useState(null); const logData = []; - console.log(`titles`, titles); - // handle incoming messages useEffect(() => { if (socket == null) return; @@ -34,6 +33,11 @@ export default function Info() { setTitles(data); }); + // Handle playstate + socket.on('playstate', (data) => { + setPlayback(data); + }); + // Ask for selection data socket.emit('get-selected'); @@ -53,6 +57,7 @@ export default function Info() { return () => { socket.off('titles'); socket.off('selected'); + socket.off('playstate'); }; }, [socket]); @@ -80,8 +85,8 @@ export default function Info() { {/* */} - - + + ); } diff --git a/client/src/features/info/Info.module.css b/client/src/features/info/Info.module.css index d72a6ab87..ae918ff7e 100644 --- a/client/src/features/info/Info.module.css +++ b/client/src/features/info/Info.module.css @@ -15,7 +15,8 @@ justify-content: space-between; } -.header { +.header, +.headerRoll { padding: 0; margin: 0; font-size: 0.9em; @@ -24,6 +25,14 @@ justify-content: space-between; } +.header { + color: #ccc; +} + +.headerRoll { + color: #2b6cb0; +} + .labelContainer { white-space: nowrap; overflow: hidden; diff --git a/client/src/features/info/InfoTitle.jsx b/client/src/features/info/InfoTitle.jsx index c27130bb0..3d44893b0 100644 --- a/client/src/features/info/InfoTitle.jsx +++ b/client/src/features/info/InfoTitle.jsx @@ -6,7 +6,7 @@ import style from './Info.module.css'; export default function InfoTitle(props) { const [collapsed, setCollapsed] = useState(false); - const { title, data } = props; + const { title, data, roll } = props; const noTitl = data.title == null || data.title === ''; const noPres = data.presenter == null || data.presenter === ''; @@ -15,7 +15,7 @@ export default function InfoTitle(props) { return (
-
+
{title} 0) { - if (nextStart == null || wait < nextStart) { - nextStart = wait; - nextIndex = index; - } - } - } + const { + nowIndex, + nowId, + publicIndex, + nextIndex, + publicNextIndex, + timers, + timeToNext, + } = getSelectionByRoll(this._eventlist, now); // nothing to play, unload - if (foundNow == null && nextIndex == null) { + if (nowIndex === null && nextIndex === null) { this.unload(); console.log('Roll: no events found'); return; } - // we found something to play next - if (nextIndex != null) { - const e = this._eventlist[nextIndex]; - this._loadThisTitles(e, 'next'); + // there is something running, load + if (nowIndex !== null) { + // clear secondary timers + this.secondaryTimer = null; + this._secondaryTarget = null; - if (foundNow == null) { + // set timers + this._startedAt = timers._startedAt; + this._finishAt = timers._finishAt; + this.duration = timers.duration; + this.current = timers.current; + + // set selection + this.selectedEventId = nowId; + this.selectedEventIndex = nowIndex; + } + + // found something to run next + if (nextIndex != null) { + // Set running timers + if (nowIndex === null) { // only warn the first time - if (this.secondaryTimer == null) + if (this.secondaryTimer === null) console.log('Roll: waiting for event start'); // reset running timer + // ??? should this not have been reset? this.current = null; - // timer counts to nextStart - this.secondaryTimer = nextStart; - this._secondaryTarget = e.timeStart; + // timer counts to next event + this.secondaryTimer = timeToNext; + this._secondaryTarget = this._eventlist[nextIndex].timeStart; } + + // TITLES: Load next private + this._loadThisTitles(this._eventlist[nextIndex], 'next-private'); + } + + // TITLES: Load next public + if (publicNextIndex !== null) { + this._loadThisTitles(this._eventlist[publicNextIndex], 'next-public'); + } + + // TITLES: Load now private + if (nowIndex !== null) { + this._loadThisTitles(this._eventlist[nowIndex], 'now-private'); + } + // TITLES: Load now public + if (publicIndex !== null) { + this._loadThisTitles(this._eventlist[publicIndex], 'now-public'); } } diff --git a/server/src/classes/__tests__/classUtils.test.js b/server/src/classes/__tests__/classUtils.test.js new file mode 100644 index 000000000..fa666372d --- /dev/null +++ b/server/src/classes/__tests__/classUtils.test.js @@ -0,0 +1,244 @@ +import { getSelectionByRoll, sortArrayByProperty } from '../classUtils.js'; + +// test sortArrayByProperty() +describe('sort simple arrays of objects', () => { + it('sort array 1-5', () => { + const arr1 = [ + { timeStart: 1 }, + { timeStart: 5 }, + { timeStart: 3 }, + { timeStart: 2 }, + { timeStart: 4 }, + ]; + + const arr1Expected = [ + { timeStart: 1 }, + { timeStart: 2 }, + { timeStart: 3 }, + { timeStart: 4 }, + { timeStart: 5 }, + ]; + + const sorted = sortArrayByProperty(arr1, 'timeStart'); + expect(sorted).toStrictEqual(arr1Expected); + }); + + it('sort array 1-5 with null', () => { + const arr1 = [ + { timeStart: 1 }, + { timeStart: 5 }, + { timeStart: 3 }, + { timeStart: 2 }, + { timeStart: 4 }, + { timeStart: null }, + ]; + + const arr1Expected = [ + { timeStart: null }, + { timeStart: 1 }, + { timeStart: 2 }, + { timeStart: 3 }, + { timeStart: 4 }, + { timeStart: 5 }, + ]; + + const sorted = sortArrayByProperty(arr1, 'timeStart'); + expect(sorted).toStrictEqual(arr1Expected); + }); +}); + +// test getSelectionByRoll() +describe('test that roll loads selection in right order', () => { + const eventlist = [ + { + id: 1, + timeStart: 0, + timeEnd: 10, + isPublic: false, + }, + { + id: 2, + timeStart: 10, + timeEnd: 20, + isPublic: true, + }, + { + id: 3, + timeStart: 20, + timeEnd: 30, + isPublic: false, + }, + { + id: 4, + timeStart: 30, + timeEnd: 40, + isPublic: false, + }, + { + id: 5, + timeStart: 40, + timeEnd: 50, + isPublic: true, + }, + { + id: 6, + timeStart: 50, + timeEnd: 60, + isPublic: false, + }, + { + id: 7, + timeStart: 60, + timeEnd: 70, + isPublic: true, + }, + { + id: 8, + timeStart: 70, + timeEnd: 80, + isPublic: false, + }, + ]; + + it('if timer is at 0', () => { + const now = 0; + const expected = { + nowIndex: 0, + nowId: 1, + publicIndex: null, + nextIndex: 1, + publicNextIndex: 1, + timers: { + _finishAt: 10, + _startedAt: 0, + current: 10, + duration: 10, + }, + timeToNext: 10, + }; + + const state = getSelectionByRoll(eventlist, now); + expect(state).toStrictEqual(expected); + }); + + it('if timer is at 15', () => { + const now = 15; + const expected = { + nowIndex: 1, + nowId: 2, + publicIndex: 1, + nextIndex: 2, + publicNextIndex: 4, + timers: { + _finishAt: 20, + _startedAt: 10, + current: 5, + duration: 10, + }, + timeToNext: 5, + }; + + const state = getSelectionByRoll(eventlist, now); + expect(state).toStrictEqual(expected); + }); + + it('if timer is at 20', () => { + const now = 20; + const expected = { + nowIndex: 2, + nowId: 3, + publicIndex: 1, + nextIndex: 3, + publicNextIndex: 4, + timers: { + _startedAt: 20, + _finishAt: 30, + current: 10, + duration: 10, + }, + timeToNext: 10, + }; + + const state = getSelectionByRoll(eventlist, now); + expect(state).toStrictEqual(expected); + }); + + it('if timer is at 49', () => { + const now = 49; + const expected = { + nowIndex: 4, + nowId: 5, + publicIndex: 4, + nextIndex: 5, + publicNextIndex: 6, + timers: { + _startedAt: 40, + _finishAt: 50, + current: 1, + duration: 10, + }, + timeToNext: 1, + }; + + const state = getSelectionByRoll(eventlist, now); + expect(state).toStrictEqual(expected); + }); + + it('if timer is at 63', () => { + const now = 63; + const expected = { + nowIndex: 6, + nowId: 7, + publicIndex: 6, + nextIndex: 7, + publicNextIndex: null, + timers: { + _startedAt: 60, + _finishAt: 70, + current: 7, + duration: 10, + }, + timeToNext: 7, + }; + + const state = getSelectionByRoll(eventlist, now); + expect(state).toStrictEqual(expected); + }); + + it('if timer is at 75', () => { + const now = 75; + const expected = { + nowIndex: 7, + nowId: 8, + publicIndex: 6, + nextIndex: null, + publicNextIndex: null, + timers: { + _startedAt: 70, + _finishAt: 80, + current: 5, + duration: 10, + }, + timeToNext: null, + }; + + const state = getSelectionByRoll(eventlist, now); + expect(state).toStrictEqual(expected); + }); + + it('if timer is at 100', () => { + const now = 100; + const expected = { + nowIndex: null, + nowId: null, + publicIndex: null, + nextIndex: null, + publicNextIndex: null, + timers: null, + timeToNext: null, + }; + + const state = getSelectionByRoll(eventlist, now); + expect(state).toStrictEqual(expected); + }); +}); diff --git a/server/src/classes/classUtils.js b/server/src/classes/classUtils.js new file mode 100644 index 000000000..3c2ab2bfc --- /dev/null +++ b/server/src/classes/classUtils.js @@ -0,0 +1,120 @@ +/** + * @description Sorts an array of objects by given property + * @param {array} arr - array to be sorted + * @param {string} property - property to compare + * @returns {array} copy of array sorted in ascending order + */ + +export const sortArrayByProperty = (arr, property) => { + return [...arr].sort((a, b) => { + return a[property] - b[property]; + }); +}; + +/** + * @description Used in roll mode, returns selection variables from array + * @param {array} arr - event list + * @param {number} now - time now in millis + * @returns {object} object with selection variables + */ + +export const getSelectionByRoll = (arr, now) => { + // Events now + let nowIndex = null; // index of event now + let nowId = null; // id of event now + let publicIndex = null; // index of public event now + let publicTime = -1; // counter: + + // Events next + let nextIndex = null; // index of next event + let publicNextIndex = null; // index of next public event + let timeToNext = null; // counter: time for next event + let publicTimeToNext = null; // counter: time for next public event + + // current timer + let timers = null; + + // Order events by startTime + const orderedEvents = sortArrayByProperty(arr, 'timeStart'); + + // exit early if we are past the events + const lastEventEnd = orderedEvents[orderedEvents.length - 1].timeEnd; + if (now > lastEventEnd) { + return { + nowIndex, + nowId, + publicIndex, + nextIndex, + publicNextIndex, + timers, + timeToNext, + }; + } + + // loop through events, look for where we should be + for (const e of orderedEvents) { + // When does the event end (handle midnight) + const normalEnd = + e.timeEnd < e.timeStart ? (e.timeEnd += this.DAYMS) : e.timeEnd; + + if (normalEnd < now) { + // event ran already + + // public event might not be the one running + if (e.isPublic && normalEnd > publicTime) { + publicTime = normalEnd; + publicIndex = arr.findIndex((a) => a.id === e.id); + } + } else if (normalEnd >= now && now >= e.timeStart) { + // event is running + + // it could also be public + if (e.isPublic) { + publicTime = normalEnd; + publicIndex = arr.findIndex((a) => a.id === e.id); + } + + nowIndex = arr.findIndex((a) => a.id === e.id); + nowId = e.id; + + // set timers + timers = { + _startedAt: e.timeStart, + _finishAt: normalEnd, + duration: normalEnd - e.timeStart, + current: normalEnd - now, + }; + } else if (normalEnd > now) { + // event will run + + // no need to look after found first + if (nextIndex !== null && publicNextIndex !== null) continue; + + // look for next events + // check how far the start is from now + const wait = e.timeStart - now; + if (wait > 0) { + if (nextIndex === null || wait < timeToNext) { + timeToNext = wait; + nextIndex = arr.findIndex((a) => a.id === e.id); + } + if ( + (publicNextIndex === null || wait < publicTimeToNext) && + e.isPublic + ) { + publicTimeToNext = wait; + publicNextIndex = arr.findIndex((a) => a.id === e.id); + } + } + } + } + return { + nowIndex, + nowId, + publicIndex, + nextIndex, + publicNextIndex, + timers, + timeToNext, + }; +};