diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 70e3fb8bd3..5f73d82b2b 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -24,6 +24,7 @@ const Operator = React.lazy(() => import('./features/operator/OperatorExport')); const TimerView = React.lazy(() => import('./features/viewers/timer/Timer')); const MinimalTimerView = React.lazy(() => import('./features/viewers/minimal-timer/MinimalTimer')); +const PopOutTimer = React.lazy(() => import('./features/viewers/pop-out-clock/PopOutTimer')); const ClockView = React.lazy(() => import('./features/viewers/clock/Clock')); const Countdown = React.lazy(() => import('./features/viewers/countdown/Countdown')); @@ -36,6 +37,7 @@ const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo')) const STimer = withPreset(withData(TimerView)); const SMinimalTimer = withPreset(withData(MinimalTimerView)); +const SPopOutTimer = withPreset(withData(PopOutTimer)); const SClock = withPreset(withData(ClockView)); const SCountdown = withPreset(withData(Countdown)); const SBackstage = withPreset(withData(Backstage)); @@ -86,6 +88,14 @@ export default function AppRouter() { } /> + + + + } + /> (false); + const [searchParams] = useSearchParams(); + + const { getLocalizedString } = useTranslation(); + + useWindowTitle('Popout Timer'); + + const stageTimer = getTimerByType(false, time); + const display = getFormattedTimer(stageTimer, time.timerType, getLocalizedString('common.minutes'), { + removeSeconds: false, + removeLeadingZero: true, + }); + + // TODO: this should be tied to the params + // USER OPTIONS + const userOptions: OverridableOptions = { + size: 1, + }; + + // key: string + // Should be a hex string '#00FF00' with key colour + const key = searchParams.get('key'); + if (key) { + userOptions.keyColour = `#${key}`; + } + + // textColour: string + // Should be a hex string '#ffffff' + const textColour = searchParams.get('text'); + if (textColour) { + userOptions.textColour = `#${textColour}`; + } + + // textBackground: string + // Should be a hex string '#ffffff' + const textBackground = searchParams.get('textbg'); + if (textBackground) { + userOptions.textBackground = `#${textBackground}`; + } + + // font: string + // Should be a string with a font name 'arial' + const font = searchParams.get('font'); + if (font) { + userOptions.font = font; + } + + // size: multiplier + // Should be a number 0.0-n + const size = searchParams.get('size'); + if (size !== null && typeof size !== 'undefined') { + if (!Number.isNaN(Number(size))) { + userOptions.size = Number(size); + } + } + const stageTimerCharacters = display.replace('/:/g', '').length; + const timerFontSize = (89 / (stageTimerCharacters - 1)) * (userOptions.size || 1); + + const hideOvertime = searchParams.get('hideovertime'); + userOptions.hideOvertime = isStringBoolean(hideOvertime); + + const hideEndMessage = searchParams.get('hideendmessage'); + userOptions.hideEndMessage = isStringBoolean(hideEndMessage); + + const hideTimerSeconds = searchParams.get('hideTimerSeconds'); + userOptions.hideTimerSeconds = isStringBoolean(hideTimerSeconds); + + const showLeadingZeros = searchParams.get('showLeadingZeros'); + userOptions.removeLeadingZeros = !isStringBoolean(showLeadingZeros); + + const timerIsTimeOfDay = time.timerType === TimerType.Clock; + + const isPlaying = time.playback !== Playback.Pause; + + const shouldShowModifiers = time.timerType === TimerType.CountDown || time.countToEnd; + const finished = time.phase === TimerPhase.Overtime; + const showEndMessage = shouldShowModifiers && finished && viewSettings.endMessage && !hideEndMessage; + const showFinished = + shouldShowModifiers && finished && !userOptions?.hideOvertime && (shouldShowModifiers || showEndMessage); + + const showProgress = time.playback !== Playback.Stop; + const showWarning = shouldShowModifiers && time.phase === TimerPhase.Warning; + const showDanger = shouldShowModifiers && time.phase === TimerPhase.Danger; + + let timerColor = viewSettings.normalColor; + if (!timerIsTimeOfDay && showProgress && showWarning) timerColor = viewSettings.warningColor; + if (!timerIsTimeOfDay && showProgress && showDanger) timerColor = viewSettings.dangerColor; + + useEffect(() => { + if (pipElement) { + pipElement.timer.innerText = display; + } + }, [display, pipElement]); + + useEffect(() => { + if (pipElement) { + pipElement.background.classList.toggle('mirror', isMirrored); + if (userOptions.keyColour) pipElement.background.style.setProperty('background-color', userOptions.keyColour); + + pipElement.timer.classList.toggle('timer--paused', !isPlaying); + pipElement.timer.classList.toggle('timer--finished', !showFinished); + if (userOptions.textColour) pipElement.timer.style.setProperty('color', userOptions.textColour); + if (userOptions.textBackground) + pipElement.timer.style.setProperty('background-color', userOptions.textBackground); + if (userOptions.font) pipElement.timer.style.setProperty('font-family', userOptions.font); + pipElement.timer.style.setProperty('font-size', `${timerFontSize}vw`); + pipElement.timer.style.setProperty('--phase-color', timerColor); + } + }, [ + isMirrored, + isPlaying, + pipElement, + showFinished, + timerColor, + timerFontSize, + userOptions.font, + userOptions.justifyContent, + userOptions.keyColour, + userOptions.textBackground, + userOptions.textColour, + ]); + + const closePip = useCallback(() => { + if (pipElement) { + pipElement.pipWindow.close(); + } + }, [pipElement]); + + const openPip = useCallback(() => { + // @ts-expect-error - pip is experimental https://wicg.github.io/document-picture-in-picture/#documentpictureinpicture + window.documentPictureInPicture.requestWindow().then((pipWindow: Window) => { + // Copy style sheets over from the initial document + [...document.styleSheets].forEach((styleSheet) => { + try { + const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join(''); + const style = document.createElement('style'); + style.textContent = cssRules; + pipWindow.document.head.appendChild(style); + } catch (e) { + console.log('failed to copy css'); + } + }); + + // create the backgoind element + const background = document.createElement('div'); + background.classList.add('popout-timer'); + background.classList.toggle('mirror', isMirrored); + + pipWindow.document.body.append(background); + + // create the timer element + const timer = document.createElement('div'); + timer.classList.add('timer'); + background.append(timer); + + pipWindow.document.title = 'ONTIME'; //TODO: trying to hide or change the title bar + + setPipElement({ timer, pipWindow, background }); + + //clear state when the pip is closed + pipWindow.addEventListener( + 'pagehide', + () => { + setPipElement(false); + }, + { once: true }, + ); + }); + }, [isMirrored]); + + return ( +
+

+

+

+

+

+

+

+ + + + +
+ ); +} diff --git a/apps/client/src/viewerConfig.ts b/apps/client/src/viewerConfig.ts index 1ac97f085d..78341d0f36 100644 --- a/apps/client/src/viewerConfig.ts +++ b/apps/client/src/viewerConfig.ts @@ -1,6 +1,7 @@ export const navigatorConstants = [ { url: 'timer', label: 'Timer' }, { url: 'minimal', label: 'Minimal Timer' }, + { url: 'pop', label: 'Popout Timer' }, { url: 'clock', label: 'Wall Clock' }, { url: 'backstage', label: 'Backstage' }, { url: 'timeline', label: 'Timeline (beta)' },