mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-06 16:03:52 +00:00
v2 alpha5 (#315)
* fix: fetch in offline environments (#295) * add arm platforms to docker build --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * fix: docker build (#298) Co-authored-by: Fabian Posenau <fabian@fphome.de> * Timer: fix too many renders error when using ?progress (#305) * ux: remove duplicate showing of selected event * several small fixes and UX improvements --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Marks Polakovs <github@markspolakovs.me>
This commit is contained in:
@@ -141,3 +141,4 @@ jobs:
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"csv-stringify": "^6.2.3",
|
||||
"deepmerge": "^4.3.0",
|
||||
"framer-motion": "^8.0.2",
|
||||
"jotai": "^1.10.0",
|
||||
"luxon": "^3.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
|
||||
@@ -40,6 +40,7 @@ export default function AppRouter() {
|
||||
// navigate if is alias route
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
for (const d of data) {
|
||||
if (`/${d.alias}` === location.pathname && d.enabled) {
|
||||
navigate(`/${d.pathAndParams}`);
|
||||
@@ -48,9 +49,9 @@ export default function AppRouter() {
|
||||
}
|
||||
}, [data, location, navigate]);
|
||||
|
||||
return(
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='/' element={<Navigate to="/timer" /> } />
|
||||
<Route path='/' element={<Navigate to='/timer' />} />
|
||||
<Route path='/speaker' element={<STimer />} />
|
||||
<Route path='/presenter' element={<STimer />} />
|
||||
<Route path='/stage' element={<STimer />} />
|
||||
@@ -111,8 +112,8 @@ export default function AppRouter() {
|
||||
</FeatureWrapper>
|
||||
}
|
||||
/>
|
||||
{/* Send to default if nothing found */}
|
||||
<Route path='*' element={<Navigate to="/timer" /> } />
|
||||
{/*/!* Send to default if nothing found *!/*/}
|
||||
<Route path='*' element={<STimer />} />
|
||||
</Routes>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithStorage, selectAtom } from 'jotai/utils';
|
||||
|
||||
export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', {
|
||||
showQuickEntry: false,
|
||||
startTimeIsLastEnd: false,
|
||||
defaultPublic: false,
|
||||
});
|
||||
|
||||
export const showQuickEntryAtom = selectAtom(
|
||||
eventSettingsAtom,
|
||||
(settings) => settings.showQuickEntry
|
||||
);
|
||||
export const startTimeIsLastEndAtom = selectAtom(
|
||||
eventSettingsAtom,
|
||||
(settings) => settings.startTimeIsLastEnd
|
||||
);
|
||||
export const defaultPublicAtom = selectAtom(
|
||||
eventSettingsAtom,
|
||||
(settings) => settings.defaultPublic
|
||||
);
|
||||
|
||||
export const editorEventId = atom<string | null>(null);
|
||||
@@ -1,3 +0,0 @@
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
export const mirrorViewersAtom = atomWithStorage('ontime-viewers-mirrorViewers', false);
|
||||
@@ -35,7 +35,7 @@ export default function TextInput(props: TextInputProps) {
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
size={size}
|
||||
resize='none'
|
||||
resize={resize}
|
||||
variant='ontime-filled'
|
||||
{...textAreaProps}
|
||||
style={{ height: isFullHeight ? '100%' : undefined }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
@@ -21,9 +21,7 @@ interface TimeInputProps {
|
||||
}
|
||||
|
||||
export default function TimeInput(props: TimeInputProps) {
|
||||
const {
|
||||
name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0,
|
||||
} = props;
|
||||
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0 } = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [value, setValue] = useState('');
|
||||
@@ -51,74 +49,90 @@ export default function TimeInput(props: TimeInputProps) {
|
||||
* @description Submit handler
|
||||
* @param {string} newValue
|
||||
*/
|
||||
const handleSubmit = useCallback((newValue: string) => {
|
||||
// Check if there is anything there
|
||||
if (newValue === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let newValMillis = 0;
|
||||
|
||||
// check for known aliases
|
||||
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
const handleSubmit = useCallback(
|
||||
(newValue: string) => {
|
||||
// Check if there is anything there
|
||||
if (newValue === '') {
|
||||
return false;
|
||||
}
|
||||
} else if (newValue.startsWith('+') || newValue.startsWith('p+') || newValue.startsWith('p +')) {
|
||||
// string to pass should add to the end before
|
||||
const val = newValue.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(newValue);
|
||||
}
|
||||
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
let newValMillis = 0;
|
||||
|
||||
// check if time is different from before
|
||||
if (newValMillis === originalMillis) return false;
|
||||
// check for known aliases
|
||||
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
}
|
||||
} else if (newValue.startsWith('+') || newValue.startsWith('p+') || newValue.startsWith('p +')) {
|
||||
// string to pass should add to the end before
|
||||
const val = newValue.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(newValue);
|
||||
}
|
||||
|
||||
// validate with parent
|
||||
if (!validationHandler(name, newValMillis)) return false;
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
|
||||
// update entry
|
||||
submitHandler(name, newValMillis);
|
||||
// check if time is different from before
|
||||
if (newValMillis === originalMillis) return false;
|
||||
|
||||
return true;
|
||||
}, [delay, name, previousEnd, submitHandler, time, validationHandler]);
|
||||
// validate with parent
|
||||
if (!validationHandler(name, newValMillis)) return false;
|
||||
|
||||
// update entry
|
||||
submitHandler(name, newValMillis);
|
||||
|
||||
return true;
|
||||
},
|
||||
[delay, name, previousEnd, submitHandler, time, validationHandler],
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Prepare time fields
|
||||
* @param {string} value string to be parsed
|
||||
*/
|
||||
const validateAndSubmit = useCallback((newValue: string) => {
|
||||
const success = handleSubmit(newValue);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(newValue);
|
||||
setValue(millisToString(ms + delay));
|
||||
} else {
|
||||
resetValue();
|
||||
}
|
||||
}, [delay, handleSubmit, resetValue]);
|
||||
const validateAndSubmit = useCallback(
|
||||
(newValue: string) => {
|
||||
const success = handleSubmit(newValue);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(newValue);
|
||||
setValue(millisToString(ms + delay));
|
||||
} else {
|
||||
resetValue();
|
||||
}
|
||||
},
|
||||
[delay, handleSubmit, resetValue],
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const onKeyDownHandler = useCallback((event:KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current?.blur();
|
||||
const onKeyDownHandler = useCallback(
|
||||
(event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current?.blur();
|
||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||
} else if (event.key === 'Tab') {
|
||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
inputRef.current?.blur();
|
||||
resetValue();
|
||||
}
|
||||
},
|
||||
[resetValue, validateAndSubmit],
|
||||
);
|
||||
|
||||
const onBlurHandler = useCallback(
|
||||
(event: FocusEvent<HTMLInputElement>) => {
|
||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||
} else if (event.key === 'Tab') {
|
||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
inputRef.current?.blur();
|
||||
resetValue();
|
||||
}
|
||||
}, [resetValue, validateAndSubmit]);
|
||||
},
|
||||
[validateAndSubmit],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
@@ -167,7 +181,7 @@ export default function TimeInput(props: TimeInputProps) {
|
||||
variant='ontime-filled'
|
||||
onFocus={handleFocus}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={resetValue}
|
||||
onBlur={onBlurHandler}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={8}
|
||||
|
||||
@@ -6,13 +6,12 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import { mirrorViewersAtom } from '../../atoms/ViewerSettings';
|
||||
import useClickOutside from '../../hooks/useClickOutside';
|
||||
import useFullscreen from '../../hooks/useFullscreen';
|
||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
|
||||
@@ -20,7 +19,7 @@ export default function NavigationMenu() {
|
||||
const location = useLocation();
|
||||
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
const [isMirrored, setMirrored] = useAtom(mirrorViewersAtom);
|
||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
||||
const [showButton, setShowButton] = useState(false);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -49,10 +48,10 @@ export default function NavigationMenu() {
|
||||
|
||||
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
|
||||
const handleFullscreen = () => toggleFullScreen();
|
||||
const handleMirror = () => setMirrored((prev) => !prev);
|
||||
const handleMirror = () => toggleMirror();
|
||||
|
||||
return createPortal(
|
||||
<div id='navigation-menu-portal' ref={menuRef} className={isMirrored ? style.mirror : ''}>
|
||||
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
aria-label='toggle menu'
|
||||
@@ -83,7 +82,8 @@ export default function NavigationMenu() {
|
||||
onClick={handleMirror}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && handleMirror();
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
Flip Screen
|
||||
<IoSwapVertical />
|
||||
</div>
|
||||
@@ -97,12 +97,15 @@ export default function NavigationMenu() {
|
||||
key={route.url}
|
||||
to={route.url}
|
||||
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
||||
tabIndex={0}>
|
||||
tabIndex={0}
|
||||
>
|
||||
{route.label}
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>, document.body);
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,23 +17,9 @@ export default function Schedule({ className }: ScheduleProps) {
|
||||
}
|
||||
|
||||
let selectedState: 'past' | 'now' | 'future' = 'past';
|
||||
const selectedEvent = paginatedEvents.find((event) => event.id === selectedEventId);
|
||||
|
||||
return (
|
||||
<ul className={`schedule ${className}`}>
|
||||
{selectedEvent && (
|
||||
<ScheduleItem
|
||||
key={selectedEvent.id}
|
||||
selected='now'
|
||||
timeStart={selectedEvent.timeStart}
|
||||
timeEnd={selectedEvent.timeEnd}
|
||||
title={selectedEvent.title}
|
||||
presenter={selectedEvent.presenter}
|
||||
colour={isBackstage ? selectedEvent.colour : ''}
|
||||
backstageEvent={!selectedEvent.isPublic}
|
||||
skip={selectedEvent.skip}
|
||||
/>
|
||||
)}
|
||||
{paginatedEvents.map((event) => {
|
||||
if (event.id === selectedEventId) {
|
||||
selectedState = 'now';
|
||||
|
||||
@@ -17,4 +17,4 @@ export default function useEventData() {
|
||||
});
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { OntimeRundown, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
requestPutEvent,
|
||||
requestReorderEvent,
|
||||
} from '../api/eventsApi';
|
||||
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../atoms/LocalEventSettings';
|
||||
import { useLocalEvent } from '../stores/localEvent';
|
||||
import { useEmitLog } from '../stores/logger';
|
||||
|
||||
/**
|
||||
@@ -23,8 +22,9 @@ import { useEmitLog } from '../stores/logger';
|
||||
export const useEventAction = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { emitError } = useEmitLog();
|
||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||
const { eventSettings } = useLocalEvent();
|
||||
const defaultPublic = eventSettings.defaultPublic;
|
||||
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||
|
||||
/**
|
||||
* Calls mutation to add new event
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
// Roughly from useHooks - useLocalStorage
|
||||
|
||||
/**
|
||||
* @description utility hook to handle state in local storage
|
||||
* @param key
|
||||
* @param initialValue
|
||||
*/
|
||||
export const useLocalStorage = (key, initialValue) => {
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(`ontime-${key}`);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Set value to local storage
|
||||
* @param value
|
||||
*/
|
||||
const setValue = (value) => {
|
||||
try {
|
||||
// Allow value to be a function so we have same API as useState
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value;
|
||||
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* @description utility hook to handle state in local storage
|
||||
* @param key
|
||||
* @param initialValue
|
||||
*/
|
||||
export const useLocalStorage = <T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] => {
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(`ontime-${key}`);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.storageArea === window.localStorage && event.key === key) {
|
||||
try {
|
||||
const newValue = event.newValue ? JSON.parse(event.newValue) : initialValue;
|
||||
setStoredValue(newValue);
|
||||
} catch (_) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
}, [initialValue, key]);
|
||||
|
||||
/**
|
||||
* @description Set value to local storage
|
||||
* @param value
|
||||
*/
|
||||
const setValue = (value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
// Allow value to be a function so we have same API as useState
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
return [storedValue, setValue];
|
||||
};
|
||||
@@ -77,7 +77,10 @@ export const useInfoPanel = () => {
|
||||
|
||||
export const useCuesheet = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
selectedEventIndex: state.loaded.selectedEventId,
|
||||
playback: state.playback,
|
||||
selectedEventId: state.loaded.selectedEventId,
|
||||
selectedEventIndex: state.loaded.selectedEventIndex,
|
||||
numEvents: state.loaded.numEvents,
|
||||
titleNow: state.titles.titleNow,
|
||||
});
|
||||
|
||||
@@ -92,7 +95,7 @@ export const setEventPlayback = {
|
||||
|
||||
export const useTimer = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
timer: state.timer,
|
||||
...state.timer,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector, deepCompare);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export default function createStore<T>(initialState: T) {
|
||||
let currentState = initialState;
|
||||
const listeners = new Set<(state: T) => void>();
|
||||
|
||||
return {
|
||||
get: () => currentState,
|
||||
set: (newState: T) => {
|
||||
currentState = newState;
|
||||
listeners.forEach((listener) => listener(currentState));
|
||||
},
|
||||
subscribe: (listener: (state: T) => void) => {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type EventEditorStore = {
|
||||
openId: string | null;
|
||||
setOpenEvent: (eventId: string) => void;
|
||||
removeOpenEvent: () => void;
|
||||
};
|
||||
|
||||
export const useEventEditorStore = create<EventEditorStore>()((set) => ({
|
||||
openId: null,
|
||||
setOpenEvent: (eventId: string | null) => set({ openId: eventId }),
|
||||
removeOpenEvent: () => set({ openId: null }),
|
||||
}));
|
||||
@@ -0,0 +1,57 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { booleanFromLocalStorage } from '../utils/localStorage';
|
||||
|
||||
type EventSettings = {
|
||||
showQuickEntry: boolean;
|
||||
startTimeIsLastEnd: boolean;
|
||||
defaultPublic: boolean;
|
||||
};
|
||||
|
||||
type LocalEventStore = {
|
||||
eventSettings: EventSettings;
|
||||
setLocalEventSettings: (newState: EventSettings) => void;
|
||||
setShowQuickEntry: (showQuickEntry: boolean) => void;
|
||||
setStartTimeIsLastEnd: (startTimeIsLastEnd: boolean) => void;
|
||||
setDefaultPublic: (defaultPublic: boolean) => void;
|
||||
};
|
||||
|
||||
enum LocalEventKeys {
|
||||
ShowQuickEntry = 'ontime-show-quick-entry',
|
||||
StartTimeIsLastEnd = 'ontime-start-is-last-end',
|
||||
DefaultPublic = 'ontime-default-public',
|
||||
}
|
||||
|
||||
export const useLocalEvent = create<LocalEventStore>((set) => ({
|
||||
eventSettings: {
|
||||
showQuickEntry: booleanFromLocalStorage(LocalEventKeys.ShowQuickEntry, false),
|
||||
startTimeIsLastEnd: booleanFromLocalStorage(LocalEventKeys.ShowQuickEntry, true),
|
||||
defaultPublic: booleanFromLocalStorage(LocalEventKeys.ShowQuickEntry, true),
|
||||
},
|
||||
|
||||
setLocalEventSettings: (value) =>
|
||||
set(() => {
|
||||
localStorage.setItem(LocalEventKeys.ShowQuickEntry, String(value.showQuickEntry));
|
||||
localStorage.setItem(LocalEventKeys.StartTimeIsLastEnd, String(value.startTimeIsLastEnd));
|
||||
localStorage.setItem(LocalEventKeys.DefaultPublic, String(value.defaultPublic));
|
||||
return { eventSettings: value };
|
||||
}),
|
||||
|
||||
setShowQuickEntry: (showQuickEntry) =>
|
||||
set((state) => {
|
||||
localStorage.setItem(LocalEventKeys.ShowQuickEntry, String(showQuickEntry));
|
||||
return { eventSettings: { ...state.eventSettings, showQuickEntry } };
|
||||
}),
|
||||
|
||||
setStartTimeIsLastEnd: (startTimeIsLastEnd) =>
|
||||
set((state) => {
|
||||
localStorage.setItem(LocalEventKeys.StartTimeIsLastEnd, String(startTimeIsLastEnd));
|
||||
return { eventSettings: { ...state.eventSettings, startTimeIsLastEnd } };
|
||||
}),
|
||||
|
||||
setDefaultPublic: (defaultPublic) =>
|
||||
set((state) => {
|
||||
localStorage.setItem(LocalEventKeys.DefaultPublic, String(defaultPublic));
|
||||
return { eventSettings: { ...state.eventSettings, defaultPublic } };
|
||||
}),
|
||||
}));
|
||||
@@ -19,7 +19,7 @@ export const useLogData = () => useStore(logger);
|
||||
|
||||
export const addLog = (log: Log) =>
|
||||
logger.setState((state) => ({
|
||||
logs: [...state.logs, log],
|
||||
logs: [log, ...state.logs],
|
||||
}));
|
||||
|
||||
export const clearLogs = () => logger.setState({ logs: [] });
|
||||
|
||||
@@ -16,6 +16,7 @@ export const runtimeStorePlaceholder = {
|
||||
selectedEventId: null,
|
||||
duration: null,
|
||||
timerType: null,
|
||||
endAction: null,
|
||||
},
|
||||
playback: Playback.Stop,
|
||||
timerMessage: {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { booleanFromLocalStorage } from '../utils/localStorage';
|
||||
|
||||
enum LocalEventKeys {
|
||||
Mirror = 'ontime-view-mirror',
|
||||
}
|
||||
|
||||
type ViewOptionsStore = {
|
||||
mirror: boolean;
|
||||
toggleMirror: (newValue?: boolean) => void;
|
||||
};
|
||||
|
||||
export const useViewOptionsStore = create<ViewOptionsStore>()((set) => ({
|
||||
mirror: booleanFromLocalStorage(LocalEventKeys.Mirror, false),
|
||||
toggleMirror: (newValue?: boolean) =>
|
||||
set((state) => {
|
||||
const val = typeof newValue === 'undefined' ? !state.mirror : newValue;
|
||||
localStorage.setItem(LocalEventKeys.Mirror, String(val));
|
||||
return { mirror: val };
|
||||
}),
|
||||
}));
|
||||
@@ -0,0 +1,9 @@
|
||||
export function booleanFromLocalStorage(key: string, fallback: boolean): boolean {
|
||||
const valueInStorage = localStorage.getItem(key);
|
||||
if (valueInStorage) {
|
||||
return valueInStorage === 'true';
|
||||
} else {
|
||||
localStorage.setItem(key, String(fallback));
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { Playback } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
|
||||
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
|
||||
@@ -9,7 +10,6 @@ import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
import TapButton from './TapButton';
|
||||
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
interface PlaybackTimerProps {
|
||||
playback: Playback;
|
||||
@@ -17,20 +17,20 @@ interface PlaybackTimerProps {
|
||||
|
||||
export default function PlaybackTimer(props: PlaybackTimerProps) {
|
||||
const { playback } = props;
|
||||
const data = useTimer();
|
||||
const timer = useTimer();
|
||||
|
||||
// TODO: checkout typescript in utilities
|
||||
const started = millisToString(data.timer.startedAt);
|
||||
const finish = millisToString(data.timer.expectedFinish);
|
||||
const started = millisToString(timer.startedAt);
|
||||
const finish = millisToString(timer.expectedFinish);
|
||||
const isRolling = playback === Playback.Roll;
|
||||
const isStopped = playback === Playback.Stop;
|
||||
const isWaiting = data.timer.secondaryTimer !== null && data.timer.secondaryTimer > 0 && data.timer.current === null;
|
||||
const isWaiting = timer.secondaryTimer !== null && timer.secondaryTimer > 0 && timer.current === null;
|
||||
const disableButtons = isStopped || isRolling;
|
||||
const isOvertime = data.timer.current !== null && data.timer.current < 0;
|
||||
const hasAddedTime = Boolean(data.timer.addedTime);
|
||||
const isOvertime = timer.current !== null && timer.current < 0;
|
||||
const hasAddedTime = Boolean(timer.addedTime);
|
||||
|
||||
const rollLabel = isRolling ? 'Roll mode active' : '';
|
||||
const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(data.timer.addedTime)} minutes` : '';
|
||||
const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(timer.addedTime)} minutes` : '';
|
||||
|
||||
return (
|
||||
<div className={style.timeContainer}>
|
||||
@@ -44,7 +44,7 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<TimerDisplay time={isWaiting ? data.timer.secondaryTimer : data.timer.current} />
|
||||
<TimerDisplay time={isWaiting ? timer.secondaryTimer : timer.current} />
|
||||
</div>
|
||||
{isWaiting ? (
|
||||
<div className={style.roll}>
|
||||
|
||||
@@ -3,9 +3,9 @@ import { Box, useDisclosure } from '@chakra-ui/react';
|
||||
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import UploadModal from '../../common/components/upload-modal/UploadModal';
|
||||
import ModalManager from '../modals/ModalManager';
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
|
||||
import ModalManager from '../modals/ModalManager';
|
||||
|
||||
import styles from './Editor.module.scss';
|
||||
|
||||
@@ -17,9 +17,7 @@ const EventEditor = lazy(() => import('../../features/event-editor/EventEditorEx
|
||||
|
||||
export default function Editor() {
|
||||
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
|
||||
|
||||
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isIntegrationModalOpen,
|
||||
onOpen: onIntegrationModalOpen,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Select, Switch } from '@chakra-ui/react';
|
||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||
import { useAtom } from 'jotai';
|
||||
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import { editorEventId } from '../../common/atoms/LocalEventSettings';
|
||||
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
||||
import ColourInput from '../../common/components/input/colour-input/ColourInput';
|
||||
import TextInput from '../../common/components/input/text-input/TextInput';
|
||||
import TimeInput from '../../common/components/input/time-input/TimeInput';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import { useEventEditorStore } from '../../common/stores/eventEditor';
|
||||
import { useEmitLog } from '../../common/stores/logger';
|
||||
import { millisToMinutes } from '../../common/utils/dateConfig';
|
||||
import getDelayTo from '../../common/utils/getDelayTo';
|
||||
@@ -23,7 +22,7 @@ export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
|
||||
|
||||
// Todo: add previous end to TimeInput fields
|
||||
export default function EventEditor() {
|
||||
const [openId] = useAtom(editorEventId);
|
||||
const { openId } = useEventEditorStore();
|
||||
const { data } = useRundown();
|
||||
const { emitWarning, emitError } = useEmitLog();
|
||||
const { updateEvent } = useEventAction();
|
||||
@@ -226,7 +225,14 @@ export default function EventEditor() {
|
||||
</div>
|
||||
<div className={`${style.column} ${style.fullHeight}`}>
|
||||
<label className={style.inputLabel}>Note</label>
|
||||
<TextInput field='note' initialText={event.note} submitHandler={handleSubmit} isTextArea isFullHeight />
|
||||
<TextInput
|
||||
field='note'
|
||||
initialText={event.note}
|
||||
submitHandler={handleSubmit}
|
||||
isTextArea
|
||||
isFullHeight
|
||||
resize='none'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box, IconButton } from '@chakra-ui/react';
|
||||
import { FiX } from '@react-icons/all-files/fi/FiX';
|
||||
import { editorEventId } from '../../common/atoms/LocalEventSettings';
|
||||
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useEventEditorStore } from '../../common/stores/eventEditor';
|
||||
|
||||
import EventEditor from './EventEditor';
|
||||
|
||||
@@ -17,7 +17,7 @@ const closeBtnStyle = {
|
||||
};
|
||||
|
||||
export default function InfoExport() {
|
||||
const [openId, setOpenId] = useAtom(editorEventId);
|
||||
const { openId, removeOpenEvent } = useEventEditorStore();
|
||||
|
||||
return (
|
||||
<Box className={`${style.eventEditor} ${!openId ? style.noEvent : ''}`}>
|
||||
@@ -25,14 +25,8 @@ export default function InfoExport() {
|
||||
<div className={style.eventEditorLayout}>
|
||||
<EventEditor />
|
||||
<div className={style.header}>
|
||||
<IconButton
|
||||
aria-label='Close Menu'
|
||||
icon={<FiX />}
|
||||
onClick={() => setOpenId(null)}
|
||||
{...closeBtnStyle}
|
||||
/>
|
||||
<IconButton aria-label='Close Menu' icon={<FiX />} onClick={removeOpenEvent} {...closeBtnStyle} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
|
||||
@@ -14,16 +14,15 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { FiEye } from '@react-icons/all-files/fi/FiEye';
|
||||
import { FiX } from '@react-icons/all-files/fi/FiX';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { useEmitLog } from '@/common/stores/logger';
|
||||
|
||||
import { version } from '../../../package.json';
|
||||
import { getLatestVersion, postSettings } from '../../common/api/ontimeApi';
|
||||
import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings';
|
||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||
import useSettings from '../../common/hooks-query/useSettings';
|
||||
import { ontimePlaceholderSettings } from '../../common/models/OntimeSettings';
|
||||
import { useLocalEvent } from '../../common/stores/localEvent';
|
||||
|
||||
import { inputProps } from './modalHelper';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
@@ -38,7 +37,7 @@ export default function AppSettingsModal() {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [hidePin, setHidePin] = useState(true);
|
||||
|
||||
const [eventSettings, saveEventSettings] = useAtom(eventSettingsAtom);
|
||||
const { eventSettings, setLocalEventSettings } = useLocalEvent();
|
||||
const [formSettings, setFormSettings] = useState(eventSettings);
|
||||
|
||||
const [updateMessage, setUpdateMessage] = useState(<a>Using ontime version: {version}</a>);
|
||||
@@ -69,7 +68,7 @@ export default function AppSettingsModal() {
|
||||
|
||||
const hasChanged = !isEqual(formSettings, eventSettings);
|
||||
if (hasChanged) {
|
||||
saveEventSettings(formSettings);
|
||||
setLocalEventSettings(formSettings);
|
||||
validation.isValid = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { FormControl, FormLabel, Input, ModalBody, Switch } from '@chakra-ui/react';
|
||||
import { FiInfo } from '@react-icons/all-files/fi/FiInfo';
|
||||
|
||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
||||
import useInfo from '../../common/hooks-query/useInfo';
|
||||
import { httpPlaceholder } from '../../common/models/Http';
|
||||
import { ontimeVars } from '../../common/models/OntimeVars';
|
||||
|
||||
import { inputProps } from './modalHelper';
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
|
||||
import style from './Modals.module.scss';
|
||||
|
||||
export default function IntegrationSettingsModal() {
|
||||
const { data, status, refetch } = useInfo();
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [formData, setFormData] = useState(httpPlaceholder);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (changed) return;
|
||||
|
||||
setFormData({
|
||||
onLoad: data?.onLoad,
|
||||
onStart: data?.onStart,
|
||||
onUpdate: data?.onUpdate,
|
||||
onPause: data?.onPause,
|
||||
onStop: data?.onStop,
|
||||
});
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const f = formData;
|
||||
const e = { status: false, message: '' };
|
||||
|
||||
// set fields with error
|
||||
if (e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
} else {
|
||||
// call API endpoint here with value of f
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
[emitError, formData],
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = useCallback(async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
}, [refetch]);
|
||||
|
||||
// Todo: make change handler
|
||||
// Todo: toggle between GET / POST
|
||||
// Todo: add test button
|
||||
// Todo: enabled should be button
|
||||
// Todo: add friendly placeholder to input
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Integrate with third party over an HTTP API
|
||||
<br />
|
||||
🔥 Changes take effect after app restart 🔥
|
||||
</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<div className={style.modalFields}>
|
||||
<div className={style.hSeparator}>Ontime event cycle</div>
|
||||
<div className={style.blockNotes}>
|
||||
<span className={style.inlineFlex}>
|
||||
<FiInfo color='#2b6cb0' fontSize='2em' />
|
||||
Add HTTP messages that ontime will send during the event cycle
|
||||
</span>
|
||||
<span className={style.labelNote}>
|
||||
You can use variables in the HTTP request URL to send data from ontime
|
||||
</span>
|
||||
<span className={style.emNote}>
|
||||
http://127.0.0.1:8088/API/?setHeadline=
|
||||
<span className={style.labelNoteInline}>$title</span>
|
||||
&setSub=<span className={style.labelNoteInline}>$presenter</span>
|
||||
</span>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
||||
Variable
|
||||
</td>
|
||||
<td className={style.labelNote}>Value</td>
|
||||
</tr>
|
||||
{ontimeVars.map((v) => (
|
||||
<tr key={v.name}>
|
||||
<td className={style.labelNote}>{v.name}</td>
|
||||
<td>{v.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={style.hSeparator}>Send HTTP</div>
|
||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
||||
On Load
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When a new event loads
|
||||
</span>
|
||||
</FormLabel>
|
||||
<div className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onLoadURL'
|
||||
value={formData?.onLoad?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onLoad: {
|
||||
...formData.onLoad,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onLoadEnable'
|
||||
value={formData?.onLoad?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onLoad: {
|
||||
...formData.onLoad,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
||||
On Start
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When an timer starts / resumes{' '}
|
||||
</span>
|
||||
</FormLabel>
|
||||
<div className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onStartURL'
|
||||
value={formData?.onStart?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStart: {
|
||||
...formData.onStart,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onStartEnable'
|
||||
value={formData?.onStart?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStart: {
|
||||
...formData.onStart,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
||||
On Update
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
At every clock tick
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onUpdate' className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onUpdateURL'
|
||||
value={formData?.onUpdate?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onUpdate: {
|
||||
...formData.onUpdate,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onUpdateEnable'
|
||||
value={formData?.onUpdate?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onUpdate: {
|
||||
...formData.onUpdate,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
||||
On Pause
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When a timer pauses
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onPause' className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onPauseURL'
|
||||
value={formData?.onPause?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onPause: {
|
||||
...formData.onPause,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onPauseEnable'
|
||||
value={formData?.onPause?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onPause: {
|
||||
...formData.onPause,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
||||
On Stop
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When an event is unloaded
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onStop' className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onStopURL'
|
||||
value={formData?.onStop?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onStop,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onStopEnable'
|
||||
value={formData?.onStop?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onStop,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
||||
On Finish
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
When an event is finished
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl id='onFinish' className={style.modalInline}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
name='onFinishURL'
|
||||
value={formData?.onFinish?.url}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onFinish,
|
||||
url: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
colorScheme='green'
|
||||
id='onFinishEnable'
|
||||
value={formData?.onFinish?.enabled}
|
||||
onChange={(event) => {
|
||||
setChanged(true);
|
||||
setFormData({
|
||||
...formData,
|
||||
onStop: {
|
||||
...formData.onStop,
|
||||
enabled: event.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<SubmitContainer
|
||||
revert={revert}
|
||||
submitting={submitting}
|
||||
changed={changed}
|
||||
status={status}
|
||||
/>
|
||||
</form>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
@@ -2,14 +2,13 @@ import { createRef, Fragment, useCallback, useContext, useEffect } from 'react';
|
||||
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { defaultPublicAtom, showQuickEntryAtom, startTimeIsLastEndAtom } from '../../common/atoms/LocalEventSettings';
|
||||
import Empty from '../../common/components/state/Empty';
|
||||
import { CursorContext } from '../../common/context/CursorContext';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||
import { useLocalEvent } from '../../common/stores/localEvent';
|
||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||
|
||||
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
||||
@@ -25,11 +24,13 @@ export default function Rundown(props: RundownProps) {
|
||||
const { entries } = props;
|
||||
const data = useRundownEditor();
|
||||
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext);
|
||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||
const { addEvent, reorderEvent } = useEventAction();
|
||||
const cursorRef = createRef<HTMLDivElement>();
|
||||
const showQuickEntry = useAtomValue(showQuickEntryAtom);
|
||||
|
||||
const { eventSettings } = useLocalEvent();
|
||||
const defaultPublic = eventSettings.defaultPublic;
|
||||
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||
const showQuickEntry = eventSettings.showQuickEntry;
|
||||
|
||||
const insertAtCursor = useCallback(
|
||||
(type: SupportedEvent | 'clone', cursor: number) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { defaultPublicAtom, editorEventId, startTimeIsLastEndAtom } from '../../common/atoms/LocalEventSettings';
|
||||
import { CursorContext } from '../../common/context/CursorContext';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import { useEventEditorStore } from '../../common/stores/eventEditor';
|
||||
import { useLocalEvent } from '../../common/stores/localEvent';
|
||||
import { useEmitLog } from '../../common/stores/logger';
|
||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||
import { calculateDuration } from '../../common/utils/timesManager';
|
||||
@@ -32,11 +32,13 @@ interface RundownEntryProps {
|
||||
export default function RundownEntry(props: RundownEntryProps) {
|
||||
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||
const { openId, removeOpenEvent } = useEventEditorStore();
|
||||
const { addEvent, updateEvent, deleteEvent } = useEventAction();
|
||||
const { moveCursorTo } = useContext(CursorContext);
|
||||
const [openId, setOpenId] = useAtom(editorEventId);
|
||||
|
||||
const { eventSettings } = useLocalEvent();
|
||||
const defaultPublic = eventSettings.defaultPublic;
|
||||
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||
|
||||
// Create / delete new events
|
||||
type FieldValue = {
|
||||
@@ -71,7 +73,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
}
|
||||
case 'delete': {
|
||||
if (openId === data.id) {
|
||||
setOpenId(null);
|
||||
removeOpenEvent();
|
||||
}
|
||||
deleteEvent(data.id);
|
||||
break;
|
||||
@@ -122,7 +124,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
moveCursorTo,
|
||||
openId,
|
||||
previousEventId,
|
||||
setOpenId,
|
||||
removeOpenEvent,
|
||||
startTimeIsLastEnd,
|
||||
updateEvent,
|
||||
],
|
||||
|
||||
@@ -10,13 +10,12 @@ import { IoReload } from '@react-icons/all-files/io5/IoReload';
|
||||
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
|
||||
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
|
||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Playback } from 'ontime-types';
|
||||
|
||||
import { editorEventId } from '../../../common/atoms/LocalEventSettings';
|
||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import { setEventPlayback } from '../../../common/hooks/useSocket';
|
||||
import { useEventEditorStore } from '../../../common/stores/eventEditor';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
import { EventItemActions } from '../RundownEntry';
|
||||
@@ -78,7 +77,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
actionHandler,
|
||||
} = props;
|
||||
|
||||
const [openId, setOpenId] = useAtom(editorEventId);
|
||||
const { openId, setOpenEvent, removeOpenEvent } = useEventEditorStore();
|
||||
const { updateEvent } = useEventAction();
|
||||
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
||||
const onFocusRef = useRef<null | HTMLSpanElement>(null);
|
||||
@@ -111,6 +110,14 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
[title, updateEvent, eventId],
|
||||
);
|
||||
|
||||
const toggleOpenEvent = useCallback(() => {
|
||||
if (openId === eventId) {
|
||||
removeOpenEvent();
|
||||
} else {
|
||||
setOpenEvent(eventId);
|
||||
}
|
||||
}, [eventId, openId, removeOpenEvent, setOpenEvent]);
|
||||
|
||||
const eventIsPlaying = selected && playback === Playback.Play;
|
||||
const playBtnStyles = { _hover: {} };
|
||||
if (!skip && eventIsPlaying) {
|
||||
@@ -221,7 +228,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
variant='ontime-subtle-white'
|
||||
size='sm'
|
||||
icon={<IoOptions />}
|
||||
clickHandler={() => setOpenId((prev) => (prev === eventId ? null : eventId))}
|
||||
clickHandler={toggleOpenEvent}
|
||||
tooltip='Event options'
|
||||
aria-label='Event options'
|
||||
tabIndex={-1}
|
||||
|
||||
@@ -11,25 +11,16 @@ interface EventBlockProgressBarProps {
|
||||
|
||||
export default function EventBlockProgressBar(props: EventBlockProgressBarProps) {
|
||||
const { playback } = props;
|
||||
const { data: timer } = useTimer();
|
||||
const timer = useTimer();
|
||||
|
||||
const now = Math.floor(Math.max((timer?.current ?? 1) / 1000, 0));
|
||||
const complete = (timer?.duration ?? 1) / 1000;
|
||||
const elapsed = clamp(100 - (now * 100) / complete, 0, 100);
|
||||
const progress = `${elapsed}%`;
|
||||
|
||||
if ((timer?.current ?? 0) < 0) {
|
||||
return (
|
||||
<div
|
||||
className={`${style.progressBar} ${style.overtime}`}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
);
|
||||
return <div className={`${style.progressBar} ${style.overtime}`} style={{ width: '100%' }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${style.progressBar} ${playback ? style[playback] : ''}`}
|
||||
style={{ width: progress }}
|
||||
/>
|
||||
);
|
||||
return <div className={`${style.progressBar} ${playback ? style[playback] : ''}`} style={{ width: progress }} />;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Button, Checkbox, Tooltip } from '@chakra-ui/react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../../../common/atoms/LocalEventSettings';
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import { useLocalEvent } from '../../../common/stores/localEvent';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
@@ -22,11 +21,14 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
|
||||
const { showKbd, eventId, previousEventId, disableAddDelay = true, disableAddBlock } = props;
|
||||
const { addEvent } = useEventAction();
|
||||
const { emitError } = useEmitLog();
|
||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||
|
||||
const doStartTime = useRef<HTMLInputElement | null>(null);
|
||||
const doPublic = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const { eventSettings } = useLocalEvent();
|
||||
const defaultPublic = eventSettings.defaultPublic;
|
||||
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||
|
||||
const handleCreateEvent = useCallback(
|
||||
(eventType: SupportedEvent) => {
|
||||
switch (eventType) {
|
||||
|
||||
@@ -20,17 +20,16 @@ import PlaybackIcon from './tableElements/PlaybackIcon';
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function TableHeader({ handleCSVExport, featureData }) {
|
||||
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } =
|
||||
useContext(TableSettingsContext);
|
||||
const { data: timer } = useTimer();
|
||||
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } = useContext(TableSettingsContext);
|
||||
const timer = useTimer();
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
const { data: event } = useEventData();
|
||||
|
||||
const selected = !featureData.numEvents
|
||||
? 'No events'
|
||||
: `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : '-'}/${
|
||||
featureData.numEvents ? featureData.numEvents : '-'
|
||||
}`;
|
||||
featureData.numEvents ? featureData.numEvents : '-'
|
||||
}`;
|
||||
|
||||
// prepare presentation variables
|
||||
const isOvertime = timer.current < 0;
|
||||
|
||||
@@ -15,8 +15,8 @@ import style from './Table.module.scss';
|
||||
export default function TableWrapper() {
|
||||
const { data: rundown } = useRundown();
|
||||
const { data: userFields } = useUserFields();
|
||||
const { data: featureData } = useCuesheet();
|
||||
const { updateEvent } = useEventAction();
|
||||
const featureData = useCuesheet();
|
||||
const { theme } = useContext(TableSettingsContext);
|
||||
|
||||
// Set window title
|
||||
@@ -30,11 +30,11 @@ export default function TableWrapper() {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if value is the same
|
||||
const event = rundown[rowIndex];
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
// check if value is the same
|
||||
const event = rundown[rowIndex];
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event[accessor] === payload) {
|
||||
return;
|
||||
@@ -52,13 +52,15 @@ export default function TableWrapper() {
|
||||
[accessor]: cleanVal,
|
||||
};
|
||||
|
||||
// submit
|
||||
try {
|
||||
await updateEvent(mutationObject);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [updateEvent, rundown]);
|
||||
// submit
|
||||
try {
|
||||
await updateEvent(mutationObject);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
[updateEvent, rundown],
|
||||
);
|
||||
|
||||
const exportHandler = useCallback(
|
||||
(headerData) => {
|
||||
@@ -75,17 +77,14 @@ export default function TableWrapper() {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
},
|
||||
[rundown, userFields]
|
||||
[rundown, userFields],
|
||||
);
|
||||
|
||||
if (typeof rundown === 'undefined' || typeof userFields === 'undefined') {
|
||||
return <span>loading...</span>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper}
|
||||
data-testid="cuesheet"
|
||||
>
|
||||
<div className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper} data-testid='cuesheet'>
|
||||
<TableHeader handleCSVExport={exportHandler} featureData={featureData} />
|
||||
<OntimeTable
|
||||
tableData={rundown}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { Playback } from 'ontime-types';
|
||||
import { Playback, TitleBlock } from 'ontime-types';
|
||||
|
||||
import useEventData from '../../common/hooks-query/useEventData';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||
import { useRuntimeStore } from '../../common/stores/runtime';
|
||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||
|
||||
export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean };
|
||||
|
||||
const withData = (Component: ReactNode) => {
|
||||
return (props) => {
|
||||
// persisted app state
|
||||
const { mirror: isMirrored } = useViewOptionsStore();
|
||||
|
||||
// HTTP API data
|
||||
const { data: eventsData } = useRundown();
|
||||
@@ -41,7 +46,7 @@ const withData = (Component: ReactNode) => {
|
||||
let showNext = true;
|
||||
if (!titles.titleNext && !titles.subtitleNext && !titles.presenterNext) showNext = false;
|
||||
|
||||
const titleManager = { ...titles, showNow: showNow, showNext: showNext };
|
||||
const titleManager: TitleManager = { ...titles, showNow: showNow, showNext: showNext };
|
||||
|
||||
/********************************************/
|
||||
/*** + publicTitleManager ***/
|
||||
@@ -56,7 +61,7 @@ const withData = (Component: ReactNode) => {
|
||||
let showPublicNext = true;
|
||||
if (!titlesPublic.titleNext && !titlesPublic.subtitleNext && !titlesPublic.presenterNext) showPublicNext = false;
|
||||
|
||||
const publicTitleManager = {
|
||||
const publicTitleManager: TitleManager = {
|
||||
...titlesPublic,
|
||||
showNow: showPublicNow,
|
||||
showNext: showPublicNext,
|
||||
@@ -85,6 +90,7 @@ const withData = (Component: ReactNode) => {
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
isMirrored={isMirrored}
|
||||
pres={timerMessage}
|
||||
publ={publicMessage}
|
||||
lower={lowerMessage}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||
import Schedule from '../../../common/components/schedule/Schedule';
|
||||
@@ -13,10 +11,12 @@ import { ScheduleProvider } from '../../../common/components/schedule/ScheduleCo
|
||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import './Backstage.scss';
|
||||
|
||||
@@ -25,21 +25,20 @@ const formatOptions = {
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
Backstage.propTypes = {
|
||||
publ: PropTypes.object,
|
||||
title: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
backstageEvents: PropTypes.array,
|
||||
selectedId: PropTypes.string,
|
||||
general: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
interface BackstageProps {
|
||||
isMirrored: boolean;
|
||||
publ: Message;
|
||||
title: TitleManager;
|
||||
time: TimeManagerType;
|
||||
backstageEvents: OntimeEvent[];
|
||||
selectedId: string | null;
|
||||
general: EventData;
|
||||
viewSettings: ViewSettings;
|
||||
}
|
||||
|
||||
// @ts-expect-error unable to type just yet
|
||||
export default function Backstage(props) {
|
||||
const { publ, title, time, backstageEvents, selectedId, general, viewSettings } = props;
|
||||
export default function Backstage(props: BackstageProps) {
|
||||
const { isMirrored, publ, title, time, backstageEvents, selectedId, general, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
import { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
@@ -14,6 +12,7 @@ import { formatTime } from '../../../common/utils/time';
|
||||
import './Clock.scss';
|
||||
|
||||
interface ClockProps {
|
||||
isMirrored: boolean;
|
||||
time: TimeManagerType;
|
||||
viewSettings: ViewSettings;
|
||||
}
|
||||
@@ -24,10 +23,9 @@ const formatOptions = {
|
||||
};
|
||||
|
||||
export default function Clock(props: ClockProps) {
|
||||
const { time, viewSettings } = props;
|
||||
const { isMirrored, time, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Clock';
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||
import PropTypes from 'prop-types';
|
||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
||||
import getDelayTo from '../../../common/utils/getDelayTo';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
@@ -27,19 +25,18 @@ const formatOptionsFinished = {
|
||||
format: 'hh:mm a',
|
||||
};
|
||||
|
||||
Countdown.propTypes = {
|
||||
backstageEvents: PropTypes.array,
|
||||
time: PropTypes.object,
|
||||
selectedId: PropTypes.string,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
interface CountdownProps {
|
||||
isMirrored: boolean;
|
||||
backstageEvents: OntimeEvent[];
|
||||
time: TimeManagerType;
|
||||
selectedId: string | null;
|
||||
viewSettings: ViewSettings;
|
||||
}
|
||||
|
||||
// @ts-expect-error we are unable to type this just yet
|
||||
export default function Countdown(props) {
|
||||
const { backstageEvents, time, selectedId, viewSettings } = props;
|
||||
export default function Countdown(props: CountdownProps) {
|
||||
const { isMirrored, backstageEvents, time, selectedId, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
const [follow, setFollow] = useState<OntimeEvent | null>(null);
|
||||
const [runningTimer, setRunningTimer] = useState(0);
|
||||
@@ -97,20 +94,15 @@ export default function Countdown(props) {
|
||||
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const startTime =
|
||||
follow === null
|
||||
? '...'
|
||||
: formatTime(follow.timeStart + delay, formatOptions);
|
||||
const endTime =
|
||||
follow === null
|
||||
? '...'
|
||||
: formatTime(follow.timeEnd + delay, formatOptions);
|
||||
const formattedTimer = runningMessage === TimerMessage.ended
|
||||
? formatTime(runningTimer, formatOptionsFinished)
|
||||
: formatDisplay(
|
||||
isSelected ? millisToSeconds(runningTimer) : millisToSeconds(runningTimer + delay),
|
||||
isSelected || time.waiting,
|
||||
);
|
||||
const startTime = follow === null ? '...' : formatTime(follow.timeStart + delay, formatOptions);
|
||||
const endTime = follow === null ? '...' : formatTime(follow.timeEnd + delay, formatOptions);
|
||||
const formattedTimer =
|
||||
runningMessage === TimerMessage.ended
|
||||
? formatTime(runningTimer, formatOptionsFinished)
|
||||
: formatDisplay(
|
||||
isSelected ? millisToSeconds(runningTimer) : millisToSeconds(runningTimer + delay),
|
||||
isSelected || time.waiting,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||
@@ -119,7 +111,6 @@ export default function Countdown(props) {
|
||||
<CountdownSelect events={backstageEvents} />
|
||||
) : (
|
||||
<div className='countdown-container' data-testid='countdown-event'>
|
||||
|
||||
<div className='clock-container'>
|
||||
<div className='label'>Time Now</div>
|
||||
<div className='time'>{clock}</div>
|
||||
@@ -127,11 +118,7 @@ export default function Countdown(props) {
|
||||
|
||||
<div className='status'>{runningMessage}</div>
|
||||
|
||||
<span
|
||||
className={`timer ${standby ? 'timer--paused' : ''} ${
|
||||
isRunningFinished ? 'timer--finished' : ''
|
||||
}`}
|
||||
>
|
||||
<span className={`timer ${standby ? 'timer--paused' : ''} ${isRunningFinished ? 'timer--finished' : ''}`}>
|
||||
{formattedTimer}
|
||||
</span>
|
||||
<div className='title'>{follow?.title || 'Untitled Event'}</div>
|
||||
@@ -139,18 +126,13 @@ export default function Countdown(props) {
|
||||
<div className='timer-group'>
|
||||
<div className='aux-timers'>
|
||||
<div className='aux-timers__label'>Start Time</div>
|
||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>
|
||||
{startTime}
|
||||
</span>
|
||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>{startTime}</span>
|
||||
</div>
|
||||
<div className='aux-timers'>
|
||||
<div className='aux-timers__label'>End Time</div>
|
||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>
|
||||
{endTime}
|
||||
</span>
|
||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>{endTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
@@ -14,6 +12,7 @@ import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
||||
import './MinimalTimer.scss';
|
||||
|
||||
interface MinimalTimerProps {
|
||||
isMirrored: boolean;
|
||||
pres: Message;
|
||||
time: TimeManagerType;
|
||||
viewSettings: ViewSettings;
|
||||
@@ -21,10 +20,9 @@ interface MinimalTimerProps {
|
||||
}
|
||||
|
||||
export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
const { pres, time, viewSettings, general } = props;
|
||||
const { isMirrored, pres, time, viewSettings, general } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Minimal Timer';
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import Schedule from '../../../common/components/schedule/Schedule';
|
||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import './Public.scss';
|
||||
|
||||
@@ -22,21 +22,20 @@ const formatOptions = {
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
Public.propTypes = {
|
||||
publ: PropTypes.object,
|
||||
publicTitle: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
events: PropTypes.array,
|
||||
publicSelectedId: PropTypes.string,
|
||||
general: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
interface BackstageProps {
|
||||
isMirrored: boolean;
|
||||
publ: Message;
|
||||
publicTitle: TitleManager;
|
||||
time: TimeManagerType;
|
||||
events: OntimeEvent[];
|
||||
publicSelectedId: string | null;
|
||||
general: EventData;
|
||||
viewSettings: ViewSettings;
|
||||
}
|
||||
|
||||
// @ts-expect-error unable to type just yet
|
||||
export default function Public(props) {
|
||||
const { publ, publicTitle, time, events, publicSelectedId, general, viewSettings } = props;
|
||||
export default function Public(props: BackstageProps) {
|
||||
const { isMirrored, publ, publicTitle, time, events, publicSelectedId, general, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Public Screen';
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import useFitText from '../../../common/hooks/useFitText';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
@@ -13,7 +12,6 @@ import { formatEventList, getEventsWithDelay, trimEventlist } from '../../../com
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
import './StudioClock.scss';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: false,
|
||||
@@ -21,6 +19,7 @@ const formatOptions = {
|
||||
};
|
||||
|
||||
StudioClock.propTypes = {
|
||||
isMirrored: PropTypes.bool,
|
||||
title: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
backstageEvents: PropTypes.array,
|
||||
@@ -31,14 +30,13 @@ StudioClock.propTypes = {
|
||||
};
|
||||
|
||||
export default function StudioClock(props) {
|
||||
const { title, time, backstageEvents, selectedId, nextId, onAir, viewSettings } = props;
|
||||
const { isMirrored, title, time, backstageEvents, selectedId, nextId, onAir, viewSettings } = props;
|
||||
|
||||
// deferring rendering seems to affect styling (font and useFitText)
|
||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
||||
|
||||
const [schedule, setSchedule] = useState([]);
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
const activeIndicators = [...Array(12).keys()];
|
||||
const secondsIndicators = [...Array(60).keys()];
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { useEffect } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Playback, TimerType } from 'ontime-types';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import './Timer.scss';
|
||||
|
||||
@@ -36,19 +35,18 @@ const titleVariants = {
|
||||
},
|
||||
};
|
||||
|
||||
Timer.propTypes = {
|
||||
general: PropTypes.object,
|
||||
pres: PropTypes.object,
|
||||
title: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
interface TimerProps {
|
||||
isMirrored: boolean;
|
||||
general: EventData;
|
||||
pres: Message;
|
||||
title: TitleManager;
|
||||
time: TimeManagerType;
|
||||
viewSettings: ViewSettings;
|
||||
}
|
||||
|
||||
// @ts-expect-error unable to type just yet
|
||||
export default function Timer(props) {
|
||||
const { general, pres, title, time, viewSettings } = props;
|
||||
export default function Timer(props: TimerProps) {
|
||||
const { isMirrored, general, pres, title, time, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Timer';
|
||||
|
||||
+13
-33
@@ -1,14 +1,4 @@
|
||||
const {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
globalShortcut,
|
||||
Tray,
|
||||
dialog,
|
||||
ipcMain,
|
||||
shell,
|
||||
Notification,
|
||||
} = require('electron');
|
||||
const { app, BrowserWindow, Menu, globalShortcut, Tray, dialog, ipcMain, shell, Notification } = require('electron');
|
||||
const path = require('path');
|
||||
const electronConfig = require('./electron.config');
|
||||
|
||||
@@ -40,14 +30,13 @@ let splash;
|
||||
let tray = null;
|
||||
|
||||
(async () => {
|
||||
|
||||
// in dev mode, we expect both UI and server to be running
|
||||
if (!isProduction) {
|
||||
return
|
||||
if (!isProduction) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ontimeServer = require(nodePath)
|
||||
const ontimeServer = require(nodePath);
|
||||
const { startDb, startServer, startOSCServer, startIntegrations } = ontimeServer;
|
||||
|
||||
await startDb();
|
||||
@@ -76,9 +65,7 @@ function showNotification(title, text) {
|
||||
function appShutdown() {
|
||||
// terminate node service
|
||||
(async () => {
|
||||
console.log('asking for shutdown 1', nodePath)
|
||||
const ontimeServer = require(nodePath)
|
||||
console.log('asking for shutdown 2', ontimeServer)
|
||||
const ontimeServer = require(nodePath);
|
||||
const { shutdown } = ontimeServer;
|
||||
await shutdown(electronConfig.appIni.shutdownCode);
|
||||
})();
|
||||
@@ -129,7 +116,8 @@ function createWindow() {
|
||||
skipTaskbar: true,
|
||||
});
|
||||
splash.setIgnoreMouseEvents(true);
|
||||
splash.loadURL(`file://${__dirname}/src/splash/splash.html`);
|
||||
const splashPath = path.join('file://', __dirname, '/src/splash/splash.html');
|
||||
splash.loadURL(splashPath);
|
||||
|
||||
win = new BrowserWindow({
|
||||
width: 1920,
|
||||
@@ -167,15 +155,13 @@ app.whenReady().then(() => {
|
||||
// (available regardless of whether app is in focus)
|
||||
// bring focus to window
|
||||
globalShortcut.register('Alt+1', () => {
|
||||
bringToFront();
|
||||
bringToFront();
|
||||
});
|
||||
|
||||
// cheat to schedule process
|
||||
setTimeout(() => {
|
||||
// Load page served by node or use React dev run
|
||||
const clientUrl = isProduction
|
||||
? electronConfig.reactAppUrl.production
|
||||
: electronConfig.reactAppUrl.development;
|
||||
const clientUrl = isProduction ? electronConfig.reactAppUrl.production : electronConfig.reactAppUrl.development;
|
||||
|
||||
win.loadURL(clientUrl).then(() => {
|
||||
win.webContents.setBackgroundThrottling(false);
|
||||
@@ -212,13 +198,13 @@ app.whenReady().then(() => {
|
||||
|
||||
// Define context menu
|
||||
const { getTrayMenu } = require('./src/menu/trayMenu.js');
|
||||
const trayMenuTemplate = getTrayMenu(bringToFront, askToQuit)
|
||||
const trayMenuTemplate = getTrayMenu(bringToFront, askToQuit);
|
||||
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
|
||||
tray.setContextMenu(trayContextMenu);
|
||||
});
|
||||
|
||||
const { getApplicationMenu } = require('./src/menu/applicationMenu.js');
|
||||
const template = getApplicationMenu(isMac, askToQuit)
|
||||
const template = getApplicationMenu(isMac, askToQuit);
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
@@ -227,16 +213,10 @@ app.once('will-quit', () => {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// Get messages from react
|
||||
// Test message
|
||||
ipcMain.on('test-message', (event, arg) => {
|
||||
showNotification('Test Message', 'test from react', arg);
|
||||
});
|
||||
|
||||
// Ask for main window reload
|
||||
// Test message
|
||||
ipcMain.on('reload', () => {
|
||||
win?.reload();
|
||||
win?.reload();
|
||||
});
|
||||
|
||||
// Terminate
|
||||
@@ -258,7 +238,7 @@ ipcMain.on('set-window', (event, arg) => {
|
||||
win.webContents.openDevTools({ mode: 'detach' });
|
||||
break;
|
||||
default:
|
||||
console.log('Electron unhandled window request', arg)
|
||||
console.log('Electron unhandled window request', arg);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Generated
-45
@@ -70,7 +70,6 @@ importers:
|
||||
eslint-plugin-simple-import-sort: ^8.0.0
|
||||
eslint-plugin-testing-library: ^5.9.1
|
||||
framer-motion: ^8.0.2
|
||||
jotai: ^1.10.0
|
||||
jsdom: ^21.1.0
|
||||
luxon: ^3.3.0
|
||||
ontime-types: workspace:*
|
||||
@@ -114,7 +113,6 @@ importers:
|
||||
csv-stringify: 6.2.3
|
||||
deepmerge: 4.3.0
|
||||
framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y
|
||||
jotai: 1.13.0_react@18.2.0
|
||||
luxon: 3.3.0
|
||||
react: 18.2.0
|
||||
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
|
||||
@@ -6219,49 +6217,6 @@ packages:
|
||||
'@sideway/pinpoint': 2.0.0
|
||||
dev: true
|
||||
|
||||
/jotai/1.13.0_react@18.2.0:
|
||||
resolution: {integrity: sha512-SPTO46Jw1aJTp2i18tiKgk++nKNKJTMNw4qYFy9QRTydceWvD2Eys+YgB7cb1S6pzbM9i0Ik/y7Yl7IH8k9Lig==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '*'
|
||||
'@babel/template': '*'
|
||||
jotai-devtools: '*'
|
||||
jotai-immer: '*'
|
||||
jotai-optics: '*'
|
||||
jotai-redux: '*'
|
||||
jotai-tanstack-query: '*'
|
||||
jotai-urql: '*'
|
||||
jotai-valtio: '*'
|
||||
jotai-xstate: '*'
|
||||
jotai-zustand: '*'
|
||||
react: '>=16.8'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@babel/template':
|
||||
optional: true
|
||||
jotai-devtools:
|
||||
optional: true
|
||||
jotai-immer:
|
||||
optional: true
|
||||
jotai-optics:
|
||||
optional: true
|
||||
jotai-redux:
|
||||
optional: true
|
||||
jotai-tanstack-query:
|
||||
optional: true
|
||||
jotai-urql:
|
||||
optional: true
|
||||
jotai-valtio:
|
||||
optional: true
|
||||
jotai-xstate:
|
||||
optional: true
|
||||
jotai-zustand:
|
||||
optional: true
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/js-sdsl/4.2.0:
|
||||
resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==}
|
||||
dev: true
|
||||
|
||||
Reference in New Issue
Block a user