feat: add loader to views

This commit is contained in:
Carlos Valente
2024-10-07 12:00:26 +02:00
committed by Carlos Valente
parent bad9dab0a0
commit 4cba970cda
12 changed files with 186 additions and 115 deletions
+66 -9
View File
@@ -14,6 +14,7 @@ import { useClientPath } from './common/hooks/useClientPath';
import Log from './features/log/Log';
import withPreset from './features/PresetWrapper';
import withData from './features/viewers/ViewWrapper';
import ViewLoader from './views/ViewLoader';
import { ONTIME_VERSION } from './ONTIME_VERSION';
import { sentryDsn, sentryRecommendedIgnore } from './sentry.config';
@@ -75,16 +76,72 @@ export default function AppRouter() {
<React.Suspense fallback={null}>
<SentryRoutes>
<Route path='/' element={<Navigate to='/timer' />} />
<Route path='/timer' element={<STimer />} />
<Route path='/public' element={<SPublic />} />
<Route path='/minimal' element={<SMinimalTimer />} />
<Route path='/clock' element={<SClock />} />
<Route path='/countdown' element={<SCountdown />} />
<Route path='/backstage' element={<SBackstage />} />
<Route path='/studio' element={<SStudio />} />
<Route
path='/timer'
element={
<ViewLoader>
<STimer />
</ViewLoader>
}
/>
<Route
path='/public'
element={
<ViewLoader>
<SPublic />
</ViewLoader>
}
/>
<Route
path='/minimal'
element={
<ViewLoader>
<SMinimalTimer />
</ViewLoader>
}
/>
<Route
path='/clock'
element={
<ViewLoader>
<SClock />
</ViewLoader>
}
/>
<Route
path='/countdown'
element={
<ViewLoader>
<SCountdown />
</ViewLoader>
}
/>
<Route
path='/backstage'
element={
<ViewLoader>
<SBackstage />
</ViewLoader>
}
/>
<Route
path='/studio'
element={
<ViewLoader>
<SStudio />
</ViewLoader>
}
/>
{/*/!* Lower third cannot have a loading screen *!/*/}
<Route path='/lower' element={<SLowerThird />} />
<Route path='/timeline' element={<STimeline />} />
<Route
path='/timeline'
element={
<ViewLoader>
<STimeline />
</ViewLoader>
}
/>
{/*/!* Protected Routes *!/*/}
<Route path='/editor' element={<Editor />} />
@@ -86,10 +86,10 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
timerType: eventNow?.timerType ?? null,
};
// prevent render until we get all the data we need
if (!viewSettings) {
return null;
}
// // prevent render until we get all the data we need
// if (!viewSettings) {
// return null;
// }
return (
<>
@@ -2,17 +2,15 @@ import { useEffect, useState } from 'react';
import QRCode from 'react-qr-code';
import { useSearchParams } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { CustomFields, OntimeEvent, ProjectData, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
import { CustomFields, OntimeEvent, ProjectData, Settings, SupportedEvent } from 'ontime-types';
import { millisToString, removeLeadingZero } from 'ontime-utils';
import { overrideStylesURL } from '../../../common/api/constants';
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
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 ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { timerPlaceholderMin } from '../../../common/utils/styleUtils';
@@ -37,25 +35,12 @@ interface BackstageProps {
backstageEvents: OntimeEvent[];
selectedId: string | null;
general: ProjectData;
viewSettings: ViewSettings;
settings: Settings | undefined;
}
export default function Backstage(props: BackstageProps) {
const {
customFields,
isMirrored,
eventNow,
eventNext,
time,
backstageEvents,
selectedId,
general,
viewSettings,
settings,
} = props;
const { customFields, isMirrored, eventNow, eventNext, time, backstageEvents, selectedId, general, settings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
const [blinkClass, setBlinkClass] = useState(false);
const [searchParams] = useSearchParams();
@@ -73,11 +58,6 @@ export default function Backstage(props: BackstageProps) {
return () => clearTimeout(timer);
}, [selectedId]);
// defer rendering until we load stylesheets
if (!shouldRender) {
return null;
}
const clock = formatTime(time.clock);
const startedAt = formatTime(time.startedAt);
const isNegative = (time.current ?? 0) < 0;
@@ -1,9 +1,7 @@
import { useSearchParams } from 'react-router-dom';
import { Settings, ViewSettings } from 'ontime-types';
import { Settings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/constants';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { OverridableOptions } from '../../../common/models/View.types';
@@ -17,22 +15,15 @@ import './Clock.scss';
interface ClockProps {
isMirrored: boolean;
time: ViewExtendedTimer;
viewSettings: ViewSettings;
settings: Settings | undefined;
}
export default function Clock(props: ClockProps) {
const { isMirrored, time, viewSettings, settings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { isMirrored, time, settings } = props;
const [searchParams] = useSearchParams();
useWindowTitle('Clock');
// defer rendering until we load stylesheets
if (!shouldRender) {
return null;
}
// get config from url: key, text, font, size, hidenav
// eg. http://localhost:3000/clock?key=f00&text=fff
// Check for user options
@@ -1,19 +1,8 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
OntimeEvent,
OntimeRundownEntry,
Playback,
Runtime,
Settings,
SupportedEvent,
TimerPhase,
ViewSettings,
} from 'ontime-types';
import { OntimeEvent, OntimeRundownEntry, Playback, Runtime, Settings, SupportedEvent, TimerPhase } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/constants';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
@@ -34,12 +23,10 @@ interface CountdownProps {
selectedId: string | null;
settings: Settings | undefined;
time: ViewExtendedTimer;
viewSettings: ViewSettings;
}
export default function Countdown(props: CountdownProps) {
const { isMirrored, backstageEvents, runtime, selectedId, settings, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { isMirrored, backstageEvents, runtime, selectedId, settings, time } = props;
const [searchParams] = useSearchParams();
const { getLocalizedString } = useTranslation();
@@ -80,11 +67,6 @@ export default function Countdown(props: CountdownProps) {
}
}, [backstageEvents, searchParams]);
// defer rendering until we load stylesheets
if (!shouldRender) {
return null;
}
const { message: runningMessage, timer: runningTimer } = fetchTimerData(time, follow, selectedId, runtime.offset);
const standby = time.playback !== Playback.Play && time.playback !== Playback.Roll && selectedId === follow?.id;
@@ -1,9 +1,7 @@
import { useSearchParams } from 'react-router-dom';
import { Playback, TimerPhase, TimerType, ViewSettings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/constants';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { OverridableOptions } from '../../../common/models/View.types';
@@ -22,17 +20,11 @@ interface MinimalTimerProps {
export default function MinimalTimer(props: MinimalTimerProps) {
const { isMirrored, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
useWindowTitle('Minimal Timer');
// defer rendering until we load stylesheets
if (!shouldRender) {
return null;
}
// TODO: this should be tied to the params
// USER OPTIONS
const userOptions: OverridableOptions = {
@@ -1,15 +1,13 @@
import QRCode from 'react-qr-code';
import { useSearchParams } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { CustomFields, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
import { CustomFields, OntimeEvent, ProjectData, Settings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/constants';
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 ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
@@ -33,7 +31,6 @@ interface BackstageProps {
events: OntimeEvent[];
publicSelectedId: string | null;
general: ProjectData;
viewSettings: ViewSettings;
settings: Settings | undefined;
}
@@ -47,21 +44,14 @@ export default function Public(props: BackstageProps) {
events,
publicSelectedId,
general,
viewSettings,
settings,
} = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
useWindowTitle('Public Schedule');
// defer rendering until we load stylesheets
if (!shouldRender) {
return null;
}
const clock = formatTime(time.clock);
const qrSize = Math.max(window.innerWidth / 15, 128);
@@ -1,12 +1,10 @@
import { useSearchParams } from 'react-router-dom';
import type { MaybeString, OntimeEvent, OntimeRundown, Settings, ViewSettings } from 'ontime-types';
import type { MaybeString, OntimeEvent, OntimeRundown, Settings } from 'ontime-types';
import { Playback } from 'ontime-types';
import { millisToString, removeSeconds, secondsInMillis } from 'ontime-utils';
import { overrideStylesURL } from '../../../common/api/constants';
import { FitText } from '../../../common/components/fit-text/FitText';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
@@ -25,24 +23,16 @@ interface StudioClockProps {
selectedId: MaybeString;
nextId: MaybeString;
onAir: boolean;
viewSettings: ViewSettings;
settings: Settings | undefined;
}
export default function StudioClock(props: StudioClockProps) {
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings, settings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, settings } = props;
const [searchParams] = useSearchParams();
useWindowTitle('Studio Clock');
// defer rendering until we load stylesheets
if (!shouldRender) {
return null;
}
const activeIndicators = [...Array(12).keys()];
const secondsIndicators = [...Array(60).keys()];
@@ -1,9 +1,7 @@
import { useMemo } from 'react';
import { MaybeString, OntimeEvent, ProjectData, Runtime, Settings, ViewSettings } from 'ontime-types';
import { MaybeString, OntimeEvent, ProjectData, Runtime, Settings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/constants';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatDuration, formatTime, getDefaultFormat } from '../../../common/utils/time';
@@ -24,7 +22,6 @@ interface TimelinePageProps {
selectedId: MaybeString;
settings: Settings | undefined;
time: ViewExtendedTimer;
viewSettings: ViewSettings;
}
/**
@@ -33,8 +30,7 @@ interface TimelinePageProps {
* There is little point splitting or memoising top level elements
*/
export default function TimelinePage(props: TimelinePageProps) {
const { backstageEvents, general, runtime, selectedId, settings, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { backstageEvents, general, runtime, selectedId, settings, time } = props;
// holds copy of the rundown with only relevant events
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(backstageEvents, selectedId);
const { getLocalizedString } = useTranslation();
@@ -46,10 +42,6 @@ export default function TimelinePage(props: TimelinePageProps) {
useWindowTitle('Timeline');
if (!shouldRender) {
return null;
}
// populate options
const defaultFormat = getDefaultFormat(settings?.timeFormat);
const progressOptions = getTimelineOptions(defaultFormat);
@@ -12,12 +12,10 @@ import {
ViewSettings,
} from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/constants';
import { FitText } from '../../../common/components/fit-text/FitText';
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
import TitleCard from '../../../common/components/title-card/TitleCard';
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { formatTime, getDefaultFormat } from '../../../common/utils/time';
@@ -62,17 +60,11 @@ interface TimerProps {
export default function Timer(props: TimerProps) {
const { auxTimer, customFields, eventNow, eventNext, isMirrored, message, settings, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
const [searchParams] = useSearchParams();
useWindowTitle('Timer');
// defer rendering until we load stylesheets
if (!shouldRender) {
return null;
}
// USER OPTIONS
const userOptions = {
hideClock: false,
@@ -0,0 +1,73 @@
@use '../theme/viewerDefs' as *;
$dot-size: 0.5rem;
$dot-spacing: 1.5rem;
.loader {
display: grid;
place-items: center;
background-color: var(--background-color-override, $viewer-background-color);
height: 100vh;
}
.ellipsis {
display: inline-block;
position: relative;
width: 5rem;
div {
position: absolute;
width: $dot-size;
height: $dot-size;
border-radius: 50%;
background-color: var(--accent-color-override, $ontime-color);
animation-timing-function: cubic-bezier(0, 1, 1, 0);
&:nth-child(1) {
left: $dot-size;
animation: lds-ellipsis1 0.6s infinite;
}
&:nth-child(2) {
left: $dot-size;
animation: lds-ellipsis2 0.6s infinite;
}
&:nth-child(3) {
left: calc($dot-size + $dot-spacing);
animation: lds-ellipsis2 0.6s infinite;
}
&:nth-child(4) {
left: calc($dot-size + 2 * $dot-spacing);
animation: lds-ellipsis3 0.6s infinite;
}
}
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate($dot-spacing, 0);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
+32
View File
@@ -0,0 +1,32 @@
import { PropsWithChildren } from 'react';
import { overrideStylesURL } from '../common/api/constants';
import { useRuntimeStylesheet } from '../common/hooks/useRuntimeStylesheet';
import useViewSettings from '../common/hooks-query/useViewSettings';
import style from './ViewLoader.module.scss';
export default function ViewLoader({ children }: PropsWithChildren) {
const { data } = useViewSettings();
const { shouldRender } = useRuntimeStylesheet(data.overrideStyles && overrideStylesURL);
// eventually we would want to leverage suspense here
// while the feature is not ready, we simply trigger a loader
// suspense would have the advantage of being triggered also by react-query
if (!shouldRender) {
return (
<div className={style.loader}>
<div className={style.ellipsis}>
<div />
<div />
<div />
<div />
</div>
</div>
);
}
// eslint-disable-next-line react/jsx-no-useless-fragment -- ensuring JSX return
return <>{children}</>;
}