diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 37a9ddd0b6..1307b0433d 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -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() { } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + {/*/!* Lower third cannot have a loading screen *!/*/} } /> - } /> + + + + } + /> {/*/!* Protected Routes *!/*/} } /> diff --git a/apps/client/src/features/viewers/ViewWrapper.tsx b/apps/client/src/features/viewers/ViewWrapper.tsx index f44a41103c..5bda5a1064 100644 --- a/apps/client/src/features/viewers/ViewWrapper.tsx +++ b/apps/client/src/features/viewers/ViewWrapper.tsx @@ -86,10 +86,10 @@ const withData =

(Component: ComponentType

) => { 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 ( <> diff --git a/apps/client/src/features/viewers/backstage/Backstage.tsx b/apps/client/src/features/viewers/backstage/Backstage.tsx index 1987232f7d..b75e10c927 100644 --- a/apps/client/src/features/viewers/backstage/Backstage.tsx +++ b/apps/client/src/features/viewers/backstage/Backstage.tsx @@ -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 { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); + const { customFields, isMirrored, eventNow, eventNext, time, backstageEvents, selectedId, general, settings } = props; + 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; diff --git a/apps/client/src/features/viewers/clock/Clock.tsx b/apps/client/src/features/viewers/clock/Clock.tsx index b95f46d803..82ebe14190 100644 --- a/apps/client/src/features/viewers/clock/Clock.tsx +++ b/apps/client/src/features/viewers/clock/Clock.tsx @@ -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 diff --git a/apps/client/src/features/viewers/countdown/Countdown.tsx b/apps/client/src/features/viewers/countdown/Countdown.tsx index 40ae2f3d39..063a411b4e 100644 --- a/apps/client/src/features/viewers/countdown/Countdown.tsx +++ b/apps/client/src/features/viewers/countdown/Countdown.tsx @@ -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 { overrideStylesURL } from '../../../common/api/constants'; +import { OntimeEvent, OntimeRundownEntry, Playback, Runtime, Settings, SupportedEvent, TimerPhase } from 'ontime-types'; + 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; diff --git a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx index 2a6f23e316..a06ff395c5 100644 --- a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx +++ b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx @@ -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 = { diff --git a/apps/client/src/features/viewers/public/Public.tsx b/apps/client/src/features/viewers/public/Public.tsx index d88c811ce9..50271db59d 100644 --- a/apps/client/src/features/viewers/public/Public.tsx +++ b/apps/client/src/features/viewers/public/Public.tsx @@ -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); diff --git a/apps/client/src/features/viewers/studio/StudioClock.tsx b/apps/client/src/features/viewers/studio/StudioClock.tsx index 7a435b5797..57a27da3dd 100644 --- a/apps/client/src/features/viewers/studio/StudioClock.tsx +++ b/apps/client/src/features/viewers/studio/StudioClock.tsx @@ -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 ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor'; import useFitText from '../../../common/hooks/useFitText'; -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,16 +23,12 @@ 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 { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, settings } = props; - // TODO: can we prevent the Flash of Unstyled Content on the 7segment fonts? - // deferring rendering seems to affect styling (font and useFitText) - useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { fontSize: titleFontSize, ref: titleRef } = useFitText({ minFontSize: 150, maxFontSize: 500 }); const [searchParams] = useSearchParams(); diff --git a/apps/client/src/features/viewers/timeline/TimelinePage.tsx b/apps/client/src/features/viewers/timeline/TimelinePage.tsx index 28cf9d9ec1..6c3d68ba6a 100644 --- a/apps/client/src/features/viewers/timeline/TimelinePage.tsx +++ b/apps/client/src/features/viewers/timeline/TimelinePage.tsx @@ -1,9 +1,7 @@ import { useMemo } from 'react'; -import { MaybeString, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types'; +import { MaybeString, OntimeEvent, ProjectData, 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 { formatTime, getDefaultFormat } from '../../../common/utils/time'; @@ -23,7 +21,6 @@ interface TimelinePageProps { selectedId: MaybeString; settings: Settings | undefined; time: ViewExtendedTimer; - viewSettings: ViewSettings; } /** @@ -32,8 +29,7 @@ interface TimelinePageProps { * There is little point splitting or memoising top level elements */ export default function TimelinePage(props: TimelinePageProps) { - const { backstageEvents, general, selectedId, settings, time, viewSettings } = props; - const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); + const { backstageEvents, general, selectedId, settings, time } = props; // holds copy of the rundown with only relevant events const { scopedRundown, firstStart, totalDuration } = useScopedRundown(backstageEvents, selectedId); const { getLocalizedString } = useTranslation(); @@ -45,10 +41,6 @@ export default function TimelinePage(props: TimelinePageProps) { useWindowTitle('Timeline'); - if (!shouldRender) { - return null; - } - // populate options const defaultFormat = getDefaultFormat(settings?.timeFormat); const progressOptions = getTimelineOptions(defaultFormat); diff --git a/apps/client/src/views/ViewLoader.module.scss b/apps/client/src/views/ViewLoader.module.scss new file mode 100644 index 0000000000..28d27cc479 --- /dev/null +++ b/apps/client/src/views/ViewLoader.module.scss @@ -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); + } +} diff --git a/apps/client/src/views/ViewLoader.tsx b/apps/client/src/views/ViewLoader.tsx new file mode 100644 index 0000000000..919d665c33 --- /dev/null +++ b/apps/client/src/views/ViewLoader.tsx @@ -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 ( +

+
+
+
+
+
+
+
+ ); + } + + // eslint-disable-next-line react/jsx-no-useless-fragment -- ensuring JSX return + return <>{children}; +}