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:
Carlos Valente
2021-11-07 13:13:06 +01:00
committed by GitHub
parent f6edb88944
commit 98e5cddad5
8 changed files with 442 additions and 70 deletions
+1 -3
View File
@@ -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/)
+9 -4
View File
@@ -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'} />
</>
);
}
+10 -1
View File
@@ -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;
+2 -2
View File
@@ -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
View File
@@ -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",
+55 -59
View File
@@ -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);
});
});
+120
View File
@@ -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,
};
};