Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DEMO] Native html pip elemnt #1300

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/client/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand All @@ -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));
Expand Down Expand Up @@ -86,6 +88,14 @@ export default function AppRouter() {
</ViewLoader>
}
/>
<Route
path='/pop'
element={
<ViewLoader>
<SPopOutTimer />
</ViewLoader>
}
/>
<Route
path='/public'
element={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { hideTimerSeconds } from '../../../common/components/view-params-editor/constants';
import { ViewOption } from '../../../common/components/view-params-editor/types';

export const POPOUT_TIMER_OPTIONS: ViewOption[] = [
{ section: 'Timer Options' },
hideTimerSeconds,
{ section: 'Element visibility' },
{
id: 'hideovertime',
title: 'Hide Overtime',
description: 'Whether to suppress overtime styles (red borders and red text)',
type: 'boolean',
defaultValue: false,
},
{
id: 'hideendmessage',
title: 'Hide End Message',
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
type: 'boolean',
defaultValue: false,
},
{ section: 'View style override' },
{
id: 'key',
title: 'Key Colour',
description: 'Background colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: 'fffff (default)',
},
{
id: 'textbg',
title: 'Text Background',
description: 'Colour of text background in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'font',
title: 'Font',
description: 'Font family, will use the fonts available in the system',
type: 'string',
placeholder: 'Arial Black (default)',
},
{
id: 'size',
title: 'Text Size',
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
type: 'number',
placeholder: '1 (default)',
},
];
65 changes: 65 additions & 0 deletions apps/client/src/features/viewers/pop-out-clock/PopOutTimer.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
@use '../../../theme/viewerDefs' as *;

.popput-timer {
margin: 0;
box-sizing: border-box; /* reset */
overflow: hidden;
width: 100%; /* restrict the page width to viewport */
height: 100vh;
transition: opacity 0.5s ease-in-out;

background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
display: grid;
place-content: center;

&--finished {
outline: clamp(4px, 1vw, 16px) solid $timer-finished-color;
outline-offset: calc(clamp(4px, 1vw, 16px) * -1);
transition: $viewer-transition-time;
}

.mirror {
transform: rotate(180deg);
}

.timer {
opacity: 1;
font-family: var(--font-family-bold-override, $timer-bold-font-family);
font-size: 20vw;
position: relative;
color: var(--timer-color-override, var(--phase-color));
transition: $viewer-transition-time;
transition-property: opacity;
background-color: transparent;
letter-spacing: 0.05em;

&--paused {
opacity: $viewer-opacity-disabled;
transition: $viewer-transition-time;
}

&--finished {
color: $timer-finished-color;
}
}

/* =================== OVERLAY ===================*/

.end-message {
text-align: center;
font-size: 12vw;
line-height: 0.9em;
font-weight: 600;
color: $timer-finished-color;
padding: 0;
}

.logo {
position: absolute;
top: 2vw;
left: 2vw;
max-width: min(200px, 20vw);
max-height: min(100px, 20vh);
}
}
217 changes: 217 additions & 0 deletions apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Button } from '@chakra-ui/react';
import { Playback, ProjectData, TimerPhase, TimerType, ViewSettings } from 'ontime-types';

import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
import { useWindowTitle } from '../../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { OverridableOptions } from '../../../common/models/View.types';
import { useTranslation } from '../../../translation/TranslationProvider';
import { getFormattedTimer, getTimerByType, isStringBoolean } from '../common/viewUtils';

import { POPOUT_TIMER_OPTIONS } from './PopOutTimer.options';

import './PopOutTimer.scss';

interface PopTimerProps {
general: ProjectData;
isMirrored: boolean;
time: ViewExtendedTimer;
viewSettings: ViewSettings;
}

export default function PopOutClock(props: PopTimerProps) {
const { general, isMirrored, time, viewSettings } = props;

Check failure on line 25 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'general' is assigned a value but never used. Allowed unused vars must match /^_/u
const [pipElement, setPipElement] = useState<
{ timer: HTMLDivElement; pipWindow: Window; background: HTMLDivElement } | false
>(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 (
<div>
<br></br>
<br></br>
<br></br>
<br></br>
<br></br>
<br></br>
<br></br>
<ViewParamsEditor viewOptions={POPOUT_TIMER_OPTIONS} />

<Button isDisabled={pipElement != false} variant='ontime-filled' onClick={openPip}>
Open
</Button>
<Button isDisabled={pipElement === false} variant='ontime-filled' onClick={closePip}>
Close
</Button>
</div>
);
}
1 change: 1 addition & 0 deletions apps/client/src/viewerConfig.ts
Original file line number Diff line number Diff line change
@@ -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)' },
Expand Down
Loading