Feat/end action (#308)

* feat: end action

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>
This commit is contained in:
Fabian Posenau
2023-03-16 19:27:15 +01:00
committed by GitHub
parent 76c8f8a4d5
commit 73533600a0
13 changed files with 79 additions and 45 deletions
-1
View File
@@ -30,7 +30,6 @@
"react-qr-code": "^2.0.11", "react-qr-code": "^2.0.11",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",
"react-use-websocket": "^4.3.1",
"typeface-open-sans": "^1.1.13", "typeface-open-sans": "^1.1.13",
"web-vitals": "^3.1.1", "web-vitals": "^3.1.1",
"zustand": "^4.3.6" "zustand": "^4.3.6"
@@ -15,9 +15,6 @@ export const RUNTIME = ['runtimeStore'];
// external stuff // external stuff
export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases/latest'; export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
// external stuff
export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
/** /**
* @description finds server path given the current location, it * @description finds server path given the current location, it
* @return {*} * @return {*}
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { Button, Select, Switch } from '@chakra-ui/react'; import { Button, Select, Switch } from '@chakra-ui/react';
import { IoBan } from '@react-icons/all-files/io5/IoBan'; import { IoBan } from '@react-icons/all-files/io5/IoBan';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { OntimeEvent, TimerType } from 'ontime-types'; import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
import { millisToString } from 'ontime-utils'; import { millisToString } from 'ontime-utils';
import { editorEventId } from '../../common/atoms/LocalEventSettings'; import { editorEventId } from '../../common/atoms/LocalEventSettings';
@@ -169,18 +169,6 @@ export default function EventEditor() {
/> />
</div> </div>
<div className={style.timeSettings}> <div className={style.timeSettings}>
<label className={style.inputLabel}>Timer Behaviour</label>
<Select
size='sm'
name='timerBehaviour'
value={event.timerBehaviour}
onChange={(event) => handleChange('timerBehaviour', event.target.value)}
>
<option value='start-end'>Start to end</option>
<option value='duration'>Duration</option>
<option value='follow-previous'>Follow previous</option>
<option value='start-only'>Start only</option>
</Select>
<label className={style.inputLabel}>Timer Type</label> <label className={style.inputLabel}>Timer Type</label>
<Select <Select
size='sm' size='sm'
@@ -192,6 +180,18 @@ export default function EventEditor() {
<option value={TimerType.CountUp}>Count up</option> <option value={TimerType.CountUp}>Count up</option>
<option value={TimerType.Clock}>Clock</option> <option value={TimerType.Clock}>Clock</option>
</Select> </Select>
<label className={style.inputLabel}>End Action</label>
<Select
size='sm'
name='endAction'
value={event.endAction}
onChange={(event) => handleChange('endAction', event.target.value)}
>
<option value={EndAction.Continue}>Continue</option>
<option value={EndAction.Stop}>Stop</option>
<option value={EndAction.LoadNext}>Load Next</option>
<option value={EndAction.PlayNext}>Play Next</option>
</Select>
<span className={style.spacer} /> <span className={style.spacer} />
<label className={`${style.inputLabel} ${style.publicToggle}`}> <label className={`${style.inputLabel} ${style.publicToggle}`}>
<Switch isChecked={event.isPublic} onChange={() => togglePublic(event.isPublic)} variant='ontime' /> <Switch isChecked={event.isPublic} onChange={() => togglePublic(event.isPublic)} variant='ontime' />
@@ -98,7 +98,7 @@ export function dispatchFromAdapter(type: string, payload: unknown, source?: 'os
} }
case 'startid': { case 'startid': {
if (!payload) { if (!payload || typeof payload !== 'string') {
throw new Error(`Event ID not recognised: ${payload}`); throw new Error(`Event ID not recognised: ${payload}`);
} }
PlaybackService.startById(payload); PlaybackService.startById(payload);
+2 -2
View File
@@ -1,11 +1,11 @@
import { OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types'; import { EndAction, OntimeBlock, OntimeDelay, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
export const event: Omit<OntimeEvent, 'id'> = { export const event: Omit<OntimeEvent, 'id'> = {
title: '', title: '',
subtitle: '', subtitle: '',
presenter: '', presenter: '',
note: '', note: '',
timerBehaviour: 'start-end', endAction: EndAction.Continue,
timerType: TimerType.CountDown, timerType: TimerType.CountDown,
timeStart: 0, timeStart: 0,
timeEnd: 0, timeEnd: 0,
+34 -9
View File
@@ -1,4 +1,4 @@
import { Playback } from 'ontime-types'; import { OntimeEvent, Playback } from 'ontime-types';
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js'; import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
import { eventStore } from '../stores/EventStore.js'; import { eventStore } from '../stores/EventStore.js';
@@ -13,10 +13,10 @@ import { logger } from '../classes/Logger.js';
export class PlaybackService { export class PlaybackService {
/** /**
* makes calls for loading and starting given event * makes calls for loading and starting given event
* @param {object} event * @param {OntimeEvent} event
* @return {boolean} success * @return {boolean} success
*/ */
static loadEvent(event) { static loadEvent(event: OntimeEvent): boolean {
let success = false; let success = false;
if (!event) { if (!event) {
logger.error('PLAYBACK', 'No event found'); logger.error('PLAYBACK', 'No event found');
@@ -36,7 +36,7 @@ export class PlaybackService {
* @param {string} eventId * @param {string} eventId
* @return {boolean} success * @return {boolean} success
*/ */
static startById(eventId) { static startById(eventId: string): boolean {
const event = EventLoader.getEventWithId(eventId); const event = EventLoader.getEventWithId(eventId);
const success = PlaybackService.loadEvent(event); const success = PlaybackService.loadEvent(event);
if (success) { if (success) {
@@ -51,7 +51,7 @@ export class PlaybackService {
* @param {number} eventIndex * @param {number} eventIndex
* @return {boolean} success * @return {boolean} success
*/ */
static startByIndex(eventIndex) { static startByIndex(eventIndex: number): boolean {
const event = EventLoader.getEventAtIndex(eventIndex); const event = EventLoader.getEventAtIndex(eventIndex);
const success = PlaybackService.loadEvent(event); const success = PlaybackService.loadEvent(event);
if (success) { if (success) {
@@ -66,7 +66,7 @@ export class PlaybackService {
* @param {string} eventId * @param {string} eventId
* @return {boolean} success * @return {boolean} success
*/ */
static loadById(eventId) { static loadById(eventId: string): boolean {
const event = EventLoader.getEventWithId(eventId); const event = EventLoader.getEventWithId(eventId);
const success = PlaybackService.loadEvent(event); const success = PlaybackService.loadEvent(event);
if (success) { if (success) {
@@ -80,7 +80,7 @@ export class PlaybackService {
* @param {number} eventIndex * @param {number} eventIndex
* @return {boolean} success * @return {boolean} success
*/ */
static loadByIndex(eventIndex) { static loadByIndex(eventIndex: number): boolean {
const event = EventLoader.getEventAtIndex(eventIndex); const event = EventLoader.getEventAtIndex(eventIndex);
const success = PlaybackService.loadEvent(event); const success = PlaybackService.loadEvent(event);
if (success) { if (success) {
@@ -104,14 +104,28 @@ export class PlaybackService {
/** /**
* Loads event after currently selected * Loads event after currently selected
* @param {string} [fallbackAction] - 'stop', 'pause'
* @return {boolean} success
*/ */
static loadNext() { static loadNext(fallbackAction?: 'stop' | 'pause'): boolean {
const nextEvent = eventLoader.findNext(); const nextEvent = eventLoader.findNext();
if (nextEvent) { if (nextEvent) {
const success = PlaybackService.loadEvent(nextEvent); const success = PlaybackService.loadEvent(nextEvent);
if (success) { if (success) {
logger.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`); logger.info('PLAYBACK', `Loaded event with ID ${nextEvent.id}`);
return true;
} }
} else if (fallbackAction === 'stop') {
logger.info('PLAYBACK', `No next event found! Stopping playback`);
PlaybackService.stop();
return false;
} else if (fallbackAction === 'pause') {
logger.info('PLAYBACK', `No next event found! Pausing playback`);
PlaybackService.pause();
return false;
} else {
logger.info('PLAYBACK', `No next event found! Continuing playback`);
return false;
} }
} }
@@ -126,6 +140,17 @@ export class PlaybackService {
} }
} }
/**
* Starts playback on next event
* @param {string} [fallbackAction] - 'stop', 'pause'
*/
static startNext(fallbackAction?: 'stop' | 'pause') {
const success = PlaybackService.loadNext(fallbackAction);
if (success) {
PlaybackService.start();
}
}
/** /**
* Pauses playback on selected event * Pauses playback on selected event
*/ */
@@ -190,7 +215,7 @@ export class PlaybackService {
* Adds delay to current event * Adds delay to current event
* @param {number} delayTime time in minutes * @param {number} delayTime time in minutes
*/ */
static setDelay(delayTime) { static setDelay(delayTime: number) {
if (eventTimer.loadedTimerId) { if (eventTimer.loadedTimerId) {
const delayInMs = delayTime * 1000 * 60; const delayInMs = delayTime * 1000 * 60;
eventTimer.delay(delayInMs); eventTimer.delay(delayInMs);
+16 -2
View File
@@ -1,4 +1,4 @@
import { Playback, TimerLifeCycle, TimerState } from 'ontime-types'; import { EndAction, Playback, TimerLifeCycle, TimerState } from 'ontime-types';
import { eventStore } from '../stores/EventStore.js'; import { eventStore } from '../stores/EventStore.js';
import { PlaybackService } from './PlaybackService.js'; import { PlaybackService } from './PlaybackService.js';
@@ -47,6 +47,7 @@ export class TimerService {
selectedEventId: null, selectedEventId: null,
duration: null, duration: null,
timerType: null, timerType: null,
endAction: null,
}; };
this.loadedTimerId = null; this.loadedTimerId = null;
this.pausedTime = 0; this.pausedTime = 0;
@@ -78,6 +79,7 @@ export class TimerService {
// update relevant information and force update // update relevant information and force update
this.timer.duration = timer.duration; this.timer.duration = timer.duration;
this.timer.timerType = timer.timerType; this.timer.timerType = timer.timerType;
this.timer.endAction = timer.endAction;
// this might not be ideal // this might not be ideal
this.timer.finishedAt = null; this.timer.finishedAt = null;
@@ -117,6 +119,7 @@ export class TimerService {
this.timer.current = timer.duration; this.timer.current = timer.duration;
this.playback = Playback.Armed; this.playback = Playback.Armed;
this.timer.timerType = timer.timerType; this.timer.timerType = timer.timerType;
this.timer.endAction = timer.endAction;
this.pausedTime = 0; this.pausedTime = 0;
this.pausedAt = 0; this.pausedAt = 0;
@@ -210,7 +213,7 @@ export class TimerService {
* Delays running timer by given amount * Delays running timer by given amount
* @param {number} amount * @param {number} amount
*/ */
delay(amount) { delay(amount: number) {
if (!this.loadedTimerId) { if (!this.loadedTimerId) {
return; return;
} }
@@ -289,6 +292,7 @@ export class TimerService {
this.pausedTime, this.pausedTime,
this.timer.clock, this.timer.clock,
); );
this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock); this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock);
} }
} }
@@ -303,6 +307,16 @@ export class TimerService {
_onFinish() { _onFinish() {
eventStore.set('timer', this.timer); eventStore.set('timer', this.timer);
integrationService.dispatch(TimerLifeCycle.onFinish); integrationService.dispatch(TimerLifeCycle.onFinish);
if (this.timer.endAction === EndAction.Stop) {
PlaybackService.stop();
} else if (this.timer.endAction === EndAction.LoadNext) {
// we need to delay here to put this action in the queue stack. otherwise it won't be executed properly
setTimeout(() => {
PlaybackService.loadNext();
}, 0);
} else if (this.timer.endAction === EndAction.PlayNext) {
PlaybackService.startNext();
}
} }
roll(currentEvent, nextEvent, timers) { roll(currentEvent, nextEvent, timers) {
+1 -1
View File
@@ -310,7 +310,7 @@ export const validateEvent = (eventArgs) => {
presenter: makeString(e.presenter, d.presenter), presenter: makeString(e.presenter, d.presenter),
timeStart: start, timeStart: start,
timeEnd: end, timeEnd: end,
timerBehaviour: makeString(e.timerBehaviour, d.timerBehaviour), endAction: makeString(e.endAction, d.endAction),
timerType: makeString(e.timerType, d.timerType), timerType: makeString(e.timerType, d.timerType),
duration: validateDuration(start, end), duration: validateDuration(start, end),
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic, isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
@@ -0,0 +1,6 @@
export enum EndAction {
Continue = 'continue',
Stop = 'stop',
LoadNext = 'load-next',
PlayNext = 'play-next',
}
@@ -1,3 +1,4 @@
import { EndAction } from '../EndAction.type.js';
import { TimerType } from '../TimerType.type.js'; import { TimerType } from '../TimerType.type.js';
export enum SupportedEvent { export enum SupportedEvent {
@@ -28,7 +29,7 @@ export type OntimeEvent = OntimeBaseEvent & {
subtitle: string; subtitle: string;
presenter: string; presenter: string;
note: string; note: string;
timerBehaviour: 'start-end', endAction: EndAction,
timerType: TimerType, timerType: TimerType,
timeStart: number; timeStart: number;
timeEnd: number; timeEnd: number;
@@ -1,4 +1,5 @@
import { TimerType } from '../TimerType.type.js'; import { TimerType } from '../TimerType.type.js';
import { EndAction } from '../EndAction.type.js';
export type TimerState = { export type TimerState = {
clock: number; // realtime clock clock: number; // realtime clock
@@ -12,4 +13,5 @@ export type TimerState = {
selectedEventId: string | null; selectedEventId: string | null;
duration: number | null; duration: number | null;
timerType: TimerType | null; timerType: TimerType | null;
endAction: EndAction | null;
}; };
+2
View File
@@ -1,5 +1,6 @@
import { Alias } from './definitions/core/Alias.type.js'; import { Alias } from './definitions/core/Alias.type.js';
import { DatabaseModel } from './definitions/DataModel.type.js'; import { DatabaseModel } from './definitions/DataModel.type.js';
import { EndAction } from './definitions/EndAction.type.js';
import { EventData } from './definitions/core/EventData.type.js'; import { EventData } from './definitions/core/EventData.type.js';
import { Message } from './definitions/runtime/MessageControl.type.js'; import { Message } from './definitions/runtime/MessageControl.type.js';
import { import {
@@ -28,6 +29,7 @@ export type { DatabaseModel };
// ---> Rundown // ---> Rundown
export { TimerType }; export { TimerType };
export { EndAction };
export { SupportedEvent }; export { SupportedEvent };
export type { OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent }; export type { OntimeBaseEvent, OntimeBlock, OntimeDelay, OntimeEvent };
export type { OntimeRundown, OntimeRundownEntry }; export type { OntimeRundown, OntimeRundownEntry };
-12
View File
@@ -85,7 +85,6 @@ importers:
react-qr-code: ^2.0.11 react-qr-code: ^2.0.11
react-router-dom: ^6.3.0 react-router-dom: ^6.3.0
react-table: ^7.7.0 react-table: ^7.7.0
react-use-websocket: ^4.3.1
sass: ^1.57.1 sass: ^1.57.1
stylelint: ^14.16.1 stylelint: ^14.16.1
stylelint-config-prettier: ^9.0.4 stylelint-config-prettier: ^9.0.4
@@ -125,7 +124,6 @@ importers:
react-qr-code: 2.0.11_react@18.2.0 react-qr-code: 2.0.11_react@18.2.0
react-router-dom: 6.6.2_biqbaboplfbrettd7655fr4n2y react-router-dom: 6.6.2_biqbaboplfbrettd7655fr4n2y
react-table: 7.8.0_react@18.2.0 react-table: 7.8.0_react@18.2.0
react-use-websocket: 4.3.1_biqbaboplfbrettd7655fr4n2y
typeface-open-sans: 1.1.13 typeface-open-sans: 1.1.13
web-vitals: 3.1.1 web-vitals: 3.1.1
zustand: 4.3.6_react@18.2.0 zustand: 4.3.6_react@18.2.0
@@ -7583,16 +7581,6 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/react-use-websocket/4.3.1_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-zHPLWrgcqydJaak2O5V9hiz4q2dwkwqNQqpgFVmSuPxLZdsZlnDs8DVHy3WtHH+A6ms/8aHIyX7+7ulOcrnR0Q==}
peerDependencies:
react: '>= 18.0.0'
react-dom: '>= 18.0.0'
dependencies:
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react/18.2.0: /react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}