mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 03:13:47 +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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user