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] Ability to differentiate between backstage and public data
- [x] Manage delays workflow - [x] Manage delays workflow
- [x] OSC Control and Feedback - [x] OSC Control and Feedback
- [x] Roll mode: run independently from system clock - [x] Roll mode: run independently using the system clock
## Unopinionated ## Unopinionated
We are not interested in forcing workflows and have made ontime so it is flexible to whichever way you would like to work. 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 appears visually broken: Please ensure that windows settings have no display zoom (it is 125% by default)
- [ ] app needs improvement on handling zoomed interfaces - [ ] app needs improvement on handling zoomed interfaces
- [ ] Very long titles might cause interface to shift - [ ] Very long titles might cause interface to shift
#### Roll
- [ ] Roll feature does not understand difference between public and backstage titles
# Help # Help
Help is underway! ... and can be viewed [here](https://cpvalente.gitbook.io/ontime/) 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: '', noteNext: '',
}); });
const [selected, setSelected] = useState('No events'); const [selected, setSelected] = useState('No events');
const [playback, setPlayback] = useState(null);
const logData = []; const logData = [];
console.log(`titles`, titles);
// handle incoming messages // handle incoming messages
useEffect(() => { useEffect(() => {
if (socket == null) return; if (socket == null) return;
@@ -34,6 +33,11 @@ export default function Info() {
setTitles(data); setTitles(data);
}); });
// Handle playstate
socket.on('playstate', (data) => {
setPlayback(data);
});
// Ask for selection data // Ask for selection data
socket.emit('get-selected'); socket.emit('get-selected');
@@ -53,6 +57,7 @@ export default function Info() {
return () => { return () => {
socket.off('titles'); socket.off('titles');
socket.off('selected'); socket.off('selected');
socket.off('playstate');
}; };
}, [socket]); }, [socket]);
@@ -80,8 +85,8 @@ export default function Info() {
</div> </div>
{/* <InfoLogger logData={logData} /> */} {/* <InfoLogger logData={logData} /> */}
<InfoNif /> <InfoNif />
<InfoTitle title={'Now'} data={titlesNow} /> <InfoTitle title={'Now'} data={titlesNow} roll={playback === 'roll'} />
<InfoTitle title={'Next'} data={titlesNext} /> <InfoTitle title={'Next'} data={titlesNext} roll={playback === 'roll'} />
</> </>
); );
} }
+10 -1
View File
@@ -15,7 +15,8 @@
justify-content: space-between; justify-content: space-between;
} }
.header { .header,
.headerRoll {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-size: 0.9em; font-size: 0.9em;
@@ -24,6 +25,14 @@
justify-content: space-between; justify-content: space-between;
} }
.header {
color: #ccc;
}
.headerRoll {
color: #2b6cb0;
}
.labelContainer { .labelContainer {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
+2 -2
View File
@@ -6,7 +6,7 @@ import style from './Info.module.css';
export default function InfoTitle(props) { export default function InfoTitle(props) {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const { title, data } = props; const { title, data, roll } = props;
const noTitl = data.title == null || data.title === ''; const noTitl = data.title == null || data.title === '';
const noPres = data.presenter == null || data.presenter === ''; const noPres = data.presenter == null || data.presenter === '';
@@ -15,7 +15,7 @@ export default function InfoTitle(props) {
return ( return (
<div className={style.container}> <div className={style.container}>
<div className={style.header}> <div className={roll ? style.headerRoll : style.header}>
{title} {title}
<Icon <Icon
className={collapsed ? style.moreCollapsed : style.moreExpanded} className={collapsed ? style.moreCollapsed : style.moreExpanded}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "0.1.3", "version": "0.1.4",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
+55 -59
View File
@@ -1,5 +1,6 @@
import { Timer } from './Timer.js'; import { Timer } from './Timer.js';
import { Server } from 'socket.io'; import { Server } from 'socket.io';
import { getSelectionByRoll } from './classUtils.js';
/* /*
* EventTimer adds functions specific to APP * EventTimer adds functions specific to APP
@@ -825,7 +826,7 @@ export class EventTimer extends Timer {
noteNext: null, noteNext: null,
}; };
this.publicTitles = { this.titlesPublic = {
titleNow: null, titleNow: null,
subtitleNow: null, subtitleNow: null,
presenterNow: null, presenterNow: null,
@@ -961,83 +962,78 @@ export class EventTimer extends Timer {
const now = this._getCurrentTime(); const now = this._getCurrentTime();
// maybe roll has already been loaded // maybe roll has already been loaded
if (this.secondaryTimer == null) { if (this.secondaryTimer === null) {
this._resetTimers(true); this._resetTimers(true);
this._resetSelection(); this._resetSelection();
} }
let foundNow = null; const {
let nextIndex = null; nowIndex,
let nextStart = null; nowId,
publicIndex,
// loop through events, look for where we should be nextIndex,
for (const [index, e] of this._eventlist.entries()) { publicNextIndex,
if (!foundNow) { timers,
let normalEnd = e.timeEnd; timeToNext,
} = getSelectionByRoll(this._eventlist, now);
// 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;
}
}
}
// nothing to play, unload // nothing to play, unload
if (foundNow == null && nextIndex == null) { if (nowIndex === null && nextIndex === null) {
this.unload(); this.unload();
console.log('Roll: no events found'); console.log('Roll: no events found');
return; return;
} }
// we found something to play next // there is something running, load
if (nextIndex != null) { if (nowIndex !== null) {
const e = this._eventlist[nextIndex]; // clear secondary timers
this._loadThisTitles(e, 'next'); 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 // only warn the first time
if (this.secondaryTimer == null) if (this.secondaryTimer === null)
console.log('Roll: waiting for event start'); console.log('Roll: waiting for event start');
// reset running timer // reset running timer
// ??? should this not have been reset?
this.current = null; this.current = null;
// timer counts to nextStart // timer counts to next event
this.secondaryTimer = nextStart; this.secondaryTimer = timeToNext;
this._secondaryTarget = e.timeStart; 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,
};
};