mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-05 15:33:59 +00:00
Fix/roll titles (#33)
Roll is now calculated with awareness of public and backstage titles Style improvements on providing feedback on roll mode
This commit is contained in:
@@ -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/)
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
{/* <InfoLogger logData={logData} /> */}
|
||||
<InfoNif />
|
||||
<InfoTitle title={'Now'} data={titlesNow} />
|
||||
<InfoTitle title={'Next'} data={titlesNext} />
|
||||
<InfoTitle title={'Now'} data={titlesNow} roll={playback === 'roll'} />
|
||||
<InfoTitle title={'Next'} data={titlesNext} roll={playback === 'roll'} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<div className={style.container}>
|
||||
<div className={style.header}>
|
||||
<div className={roll ? style.headerRoll : style.header}>
|
||||
{title}
|
||||
<Icon
|
||||
className={collapsed ? style.moreCollapsed : style.moreExpanded}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Timer } from './Timer.js';
|
||||
import { Server } from 'socket.io';
|
||||
import { getSelectionByRoll } from './classUtils.js';
|
||||
|
||||
/*
|
||||
* EventTimer adds functions specific to APP
|
||||
@@ -825,7 +826,7 @@ export class EventTimer extends Timer {
|
||||
noteNext: null,
|
||||
};
|
||||
|
||||
this.publicTitles = {
|
||||
this.titlesPublic = {
|
||||
titleNow: null,
|
||||
subtitleNow: null,
|
||||
presenterNow: null,
|
||||
@@ -961,83 +962,78 @@ export class EventTimer extends Timer {
|
||||
const now = this._getCurrentTime();
|
||||
|
||||
// maybe roll has already been loaded
|
||||
if (this.secondaryTimer == null) {
|
||||
if (this.secondaryTimer === null) {
|
||||
this._resetTimers(true);
|
||||
this._resetSelection();
|
||||
}
|
||||
|
||||
let foundNow = null;
|
||||
let nextIndex = null;
|
||||
let nextStart = null;
|
||||
|
||||
// loop through events, look for where we should be
|
||||
for (const [index, e] of this._eventlist.entries()) {
|
||||
if (!foundNow) {
|
||||
let normalEnd = e.timeEnd;
|
||||
|
||||
// handle midnight
|
||||
if (normalEnd < e.timeStart) {
|
||||
normalEnd += this.DAYMS;
|
||||
}
|
||||
|
||||
if (e.timeStart <= now && now < normalEnd) {
|
||||
// set flag
|
||||
foundNow = true;
|
||||
|
||||
// set timers
|
||||
this.secondaryTimer = null;
|
||||
this._secondaryTarget = null;
|
||||
|
||||
this._startedAt = e.timeStart;
|
||||
this._finishAt = normalEnd;
|
||||
this.duration = normalEnd - e.timeStart;
|
||||
this.current = normalEnd - now;
|
||||
|
||||
// set selection
|
||||
this.selectedEventId = e.id;
|
||||
this.selectedEventIndex = index;
|
||||
|
||||
// set titles
|
||||
this._loadTitlesNow();
|
||||
|
||||
// skip this entry for next
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// check how far the start is from now
|
||||
let wait = e.timeStart - now;
|
||||
if (wait > 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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user