mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 16:49:41 +00:00
feat: pip for timer preview (#1862)
--------- Co-authored-by: Shobhit Nagpal <74096450+Shobhit-Nagpal@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
3dfb49e63f
commit
f223766445
@@ -1,6 +1,7 @@
|
|||||||
.corner {
|
.arrow {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.corner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
@@ -21,6 +22,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.offsetCorner {
|
||||||
|
right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,33 @@
|
|||||||
import type { HTMLAttributes, LabelHTMLAttributes } from 'react';
|
import type { HTMLAttributes, JSX, LabelHTMLAttributes, MouseEventHandler } from 'react';
|
||||||
import { IconBaseProps } from 'react-icons';
|
import { IconBaseProps } from 'react-icons';
|
||||||
import { IoArrowUp } from 'react-icons/io5';
|
import { IoArrowUp } from 'react-icons/io5';
|
||||||
|
import { TbPictureInPictureOff } from 'react-icons/tb';
|
||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
import style from './EditorUtils.module.scss';
|
import style from './EditorUtils.module.scss';
|
||||||
|
|
||||||
export function Corner({ className, ...elementProps }: IconBaseProps) {
|
export function CornerExtract({ className, ...elementProps }: IconBaseProps) {
|
||||||
return <IoArrowUp className={cx([style.corner, className])} {...elementProps} />;
|
return <IoArrowUp className={cx([style.corner, style.arrow, className])} {...elementProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CornerPipButton({ className, ...elementProps }: IconBaseProps) {
|
||||||
|
return <TbPictureInPictureOff className={cx([style.corner, style.offsetCorner, className])} {...elementProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExtractAndPip extends IconBaseProps {
|
||||||
|
onExtractClick: MouseEventHandler<SVGElement>;
|
||||||
|
pipElement: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CornerWithPip({ className, pipElement, onExtractClick }: ExtractAndPip) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IoArrowUp className={cx([style.corner, style.arrow, className])} onClick={onExtractClick} />
|
||||||
|
{/* the pip element returns the icon button */}
|
||||||
|
{pipElement}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Title({ children, className, ...elementProps }: HTMLAttributes<HTMLHeadingElement>) {
|
export function Title({ children, className, ...elementProps }: HTMLAttributes<HTMLHeadingElement>) {
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ declare global {
|
|||||||
process: {
|
process: {
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
// Experimental browser feature
|
||||||
|
documentPictureInPicture: {
|
||||||
|
requestWindow: () => Promise<Window>;
|
||||||
|
window: Window;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { Corner } from '../../../common/components/editor-utils/EditorUtils';
|
import { CornerExtract } from '../../../common/components/editor-utils/EditorUtils';
|
||||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||||
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
||||||
@@ -20,7 +20,7 @@ function MessageControlExport() {
|
|||||||
return (
|
return (
|
||||||
<ProtectRoute permission='editor'>
|
<ProtectRoute permission='editor'>
|
||||||
<div className={style.messages} data-testid='panel-messages-control'>
|
<div className={style.messages} data-testid='panel-messages-control'>
|
||||||
{!isExtracted && <Corner onClick={(event) => handleLinks('messagecontrol', event)} />}
|
{!isExtracted && <CornerExtract onClick={(event) => handleLinks('messagecontrol', event)} />}
|
||||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||||
|
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { IoArrowDown, IoArrowUp, IoBan, IoTime } from 'react-icons/io5';
|
|||||||
import { LuArrowDownToLine } from 'react-icons/lu';
|
import { LuArrowDownToLine } from 'react-icons/lu';
|
||||||
import { TimerPhase, TimerType } from 'ontime-types';
|
import { TimerPhase, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { Corner } from '../../../common/components/editor-utils/EditorUtils';
|
import { CornerWithPip } from '../../../common/components/editor-utils/EditorUtils';
|
||||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||||
import { useMessagePreview } from '../../../common/hooks/useSocket';
|
import { useMessagePreview } from '../../../common/hooks/useSocket';
|
||||||
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||||
import { handleLinks } from '../../../common/utils/linkUtils';
|
import { handleLinks } from '../../../common/utils/linkUtils';
|
||||||
import { cx, timerPlaceholder } from '../../../common/utils/styleUtils';
|
import { cx, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||||
|
import PipRoot from '../../../views/editor/pip-timer/PipRoot';
|
||||||
|
|
||||||
import style from './MessageControl.module.scss';
|
import style from './MessageControl.module.scss';
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ export default function TimerPreview() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.preview}>
|
<div className={style.preview}>
|
||||||
<Corner onClick={(event) => handleLinks('timer', event)} />
|
<CornerWithPip onExtractClick={(event) => handleLinks('timer', event)} pipElement={<PipRoot />} />
|
||||||
<div className={contentClasses}>
|
<div className={contentClasses}>
|
||||||
<div
|
<div
|
||||||
className={style.mainContent}
|
className={style.mainContent}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { Corner } from '../../../common/components/editor-utils/EditorUtils';
|
import { CornerExtract } from '../../../common/components/editor-utils/EditorUtils';
|
||||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||||
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from '../../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
import ProtectRoute from '../../../common/components/protect-route/ProtectRoute';
|
||||||
@@ -18,7 +18,7 @@ function TimerControlExport() {
|
|||||||
return (
|
return (
|
||||||
<ProtectRoute permission='editor'>
|
<ProtectRoute permission='editor'>
|
||||||
<div className={style.playback} data-testid='panel-timer-control'>
|
<div className={style.playback} data-testid='panel-timer-control'>
|
||||||
{!isExtracted && <Corner onClick={(event) => handleLinks('timercontrol', event)} />}
|
{!isExtracted && <CornerExtract onClick={(event) => handleLinks('timercontrol', event)} />}
|
||||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||||
|
|
||||||
<div className={style.content}>
|
<div className={style.content}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useSessionStorage } from '@mantine/hooks';
|
import { useSessionStorage } from '@mantine/hooks';
|
||||||
|
|
||||||
import { Corner } from '../../common/components/editor-utils/EditorUtils';
|
import { CornerExtract } from '../../common/components/editor-utils/EditorUtils';
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
||||||
@@ -60,7 +60,7 @@ function RundownExport() {
|
|||||||
<div className={style.rundown}>
|
<div className={style.rundown}>
|
||||||
<div className={style.list}>
|
<div className={style.list}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{!isExtracted && <Corner onClick={(event) => handleLinks('rundown', event)} />}
|
{!isExtracted && <CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
||||||
<RundownContextMenu>
|
<RundownContextMenu>
|
||||||
<RundownWrapper />
|
<RundownWrapper />
|
||||||
</RundownContextMenu>
|
</RundownContextMenu>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { ErrorBoundary } from '@sentry/react';
|
||||||
|
|
||||||
|
import { CornerPipButton } from '../../../common/components/editor-utils/EditorUtils';
|
||||||
|
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||||
|
|
||||||
|
import { PipTimer } from './PipTimer';
|
||||||
|
|
||||||
|
export default function PipTimerHost() {
|
||||||
|
const { data, status } = useViewSettings();
|
||||||
|
|
||||||
|
const openPictureInPicture = async () => {
|
||||||
|
if (window.documentPictureInPicture.window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pipWindow: Window;
|
||||||
|
try {
|
||||||
|
pipWindow = await window.documentPictureInPicture.requestWindow();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to open Picture-in-Picture:', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[...document.styleSheets].forEach((sheet) => {
|
||||||
|
try {
|
||||||
|
if (sheet.href) {
|
||||||
|
const link = pipWindow.document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = sheet.href;
|
||||||
|
pipWindow.document.head.appendChild(link);
|
||||||
|
} else if (sheet.cssRules) {
|
||||||
|
const style = pipWindow.document.createElement('style');
|
||||||
|
style.textContent = [...sheet.cssRules].map((rule) => rule.cssText).join('');
|
||||||
|
pipWindow.document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Stylesheet copy blocked:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pipDiv = pipWindow.document.createElement('div');
|
||||||
|
pipDiv.setAttribute('id', 'pip-root');
|
||||||
|
pipDiv.style.height = '100vh';
|
||||||
|
pipWindow.document.body.append(pipDiv);
|
||||||
|
|
||||||
|
const pipRoot = createRoot(pipWindow.document.getElementById('pip-root') as Element, {
|
||||||
|
onCaughtError: (err, _errInfo) => console.error(err),
|
||||||
|
onUncaughtError: (err, _errInfo) => console.error(err),
|
||||||
|
onRecoverableError: (err, _errInfo) => console.error(err),
|
||||||
|
});
|
||||||
|
|
||||||
|
pipWindow.addEventListener('pagehide', () => {
|
||||||
|
pipRoot.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
pipRoot.render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<PipTimer viewSettings={data} />
|
||||||
|
</ErrorBoundary>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <CornerPipButton onClick={status === 'success' ? openPictureInPicture : undefined} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { lazy, memo, Suspense } from 'react';
|
||||||
|
|
||||||
|
import { isPipSupported } from './pip.utils';
|
||||||
|
|
||||||
|
const PipTimerHost = lazy(() => import('./PipHost'));
|
||||||
|
|
||||||
|
export default memo(PipRoot);
|
||||||
|
function PipRoot() {
|
||||||
|
if (!isPipSupported) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<PipTimerHost />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
/* The styles in this file are a stripped down version of the timer */
|
||||||
|
.pip-timer {
|
||||||
|
margin: 0;
|
||||||
|
padding: 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;
|
||||||
|
|
||||||
|
font-family: $viewer-font-family;
|
||||||
|
background: $viewer-background-color;
|
||||||
|
color: $viewer-color;
|
||||||
|
gap: $view-element-gap;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&--finished {
|
||||||
|
outline: clamp(4px, 1vw, 16px) solid $timer-finished-color;
|
||||||
|
outline-offset: calc(clamp(4px, 1vw, 16px) * -1);
|
||||||
|
transition: $viewer-transition-time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =================== MAIN ===================*/
|
||||||
|
|
||||||
|
.timer-container {
|
||||||
|
flex: 1;
|
||||||
|
align-content: center;
|
||||||
|
justify-self: center;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.timer {
|
||||||
|
opacity: 1;
|
||||||
|
font-family: var(--timer-font, $viewer-font-family);
|
||||||
|
color: var(--timer-colour, $ui-white);
|
||||||
|
line-height: 0.9em;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
transition-property: font-size;
|
||||||
|
transition-duration: $viewer-transition-time;
|
||||||
|
|
||||||
|
&--paused {
|
||||||
|
opacity: $viewer-opacity-disabled;
|
||||||
|
transition: $viewer-transition-time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use a class instead of a phase, to allow suppressing overtime style
|
||||||
|
&--finished {
|
||||||
|
color: var(--timer-overtime-color-override, $timer-finished-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-phase='warning'] {
|
||||||
|
color: var(--timer-colour, var(--timer-warning-color-override));
|
||||||
|
}
|
||||||
|
&[data-phase='danger'] {
|
||||||
|
color: var(--timer-colour, var(--timer-danger-color-override));
|
||||||
|
}
|
||||||
|
&[data-type='none'] {
|
||||||
|
transition: 1s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
margin-top: 0.125em;
|
||||||
|
padding-block: 0.125em;
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--external-color-override, $external-color);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
line-height: 1em;
|
||||||
|
transition-property: opacity, height;
|
||||||
|
transition-duration: $viewer-transition-time;
|
||||||
|
border-top: 1px solid color-mix(in srgb, $external-color 10%, transparent);
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
opacity: 1;
|
||||||
|
transition: $viewer-transition-time;
|
||||||
|
|
||||||
|
&--paused {
|
||||||
|
opacity: $viewer-opacity-disabled;
|
||||||
|
transition: $viewer-transition-time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =================== OVERLAY ===================*/
|
||||||
|
|
||||||
|
.message-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
padding: 2vw;
|
||||||
|
background: $viewer-background-color;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity $viewer-transition-time;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
z-index: $zindex-floating;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
color: $viewer-color;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { FitText } from '../../../common/components/fit-text/FitText';
|
||||||
|
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||||
|
import { useTimerSocket } from '../../../common/hooks/useSocket';
|
||||||
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
import { getFormattedTimer, getTimerByType } from '../../../features/viewers/common/viewUtils';
|
||||||
|
import {
|
||||||
|
getEstimatedFontSize,
|
||||||
|
getIsPlaying,
|
||||||
|
getSecondaryDisplay,
|
||||||
|
getShowMessage,
|
||||||
|
getShowModifiers,
|
||||||
|
getShowProgressBar,
|
||||||
|
getTotalTime,
|
||||||
|
} from '../../timer/timer.utils';
|
||||||
|
import { getTimerColour } from '../../utils/presentation.utils';
|
||||||
|
|
||||||
|
import './PipTimer.scss';
|
||||||
|
|
||||||
|
interface PipTimerProps {
|
||||||
|
viewSettings: ViewSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PipTimer({ viewSettings }: PipTimerProps) {
|
||||||
|
const { eventNow, message, time, clock, timerTypeNow, countToEndNow, auxTimer } = useTimerSocket();
|
||||||
|
|
||||||
|
// gather modifiers
|
||||||
|
const showOverlay = getShowMessage(message.timer);
|
||||||
|
const { showFinished, showWarning, showDanger } = getShowModifiers(
|
||||||
|
timerTypeNow,
|
||||||
|
countToEndNow,
|
||||||
|
time.phase,
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const isPlaying = getIsPlaying(time.playback);
|
||||||
|
const showProgressBar = getShowProgressBar(timerTypeNow);
|
||||||
|
|
||||||
|
// gather timer data
|
||||||
|
const totalTime = getTotalTime(time.duration, time.addedTime);
|
||||||
|
const stageTimer = getTimerByType(false, timerTypeNow, countToEndNow, clock, time, timerTypeNow);
|
||||||
|
const display = getFormattedTimer(stageTimer, timerTypeNow, 'min', {
|
||||||
|
removeSeconds: false,
|
||||||
|
removeLeadingZero: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentAux = (() => {
|
||||||
|
if (message.timer.secondarySource === 'aux1') {
|
||||||
|
return auxTimer.aux1;
|
||||||
|
}
|
||||||
|
if (message.timer.secondarySource === 'aux2') {
|
||||||
|
return auxTimer.aux2;
|
||||||
|
}
|
||||||
|
if (message.timer.secondarySource === 'aux3') {
|
||||||
|
return auxTimer.aux3;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const secondaryContent = getSecondaryDisplay(message, currentAux, 'min', false, false, false);
|
||||||
|
|
||||||
|
// gather presentation styles
|
||||||
|
const resolvedTimerColour = getTimerColour(viewSettings, undefined, showWarning, showDanger);
|
||||||
|
const { timerFontSize, externalFontSize } = getEstimatedFontSize(display, secondaryContent);
|
||||||
|
const userStyles = {
|
||||||
|
...(resolvedTimerColour && { '--timer-colour': resolvedTimerColour }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(['pip-timer', showFinished && 'pip-timer--finished'])} style={userStyles}>
|
||||||
|
<div className={cx(['message-overlay', showOverlay && 'message-overlay--active'])}>
|
||||||
|
<FitText mode='multi' min={12} max={256} className={cx(['message', message.timer.blink && 'blink'])}>
|
||||||
|
{message.timer.text}
|
||||||
|
</FitText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='timer-container'>
|
||||||
|
<div
|
||||||
|
className={cx(['timer', !isPlaying && 'timer--paused', showFinished && 'timer--finished'])}
|
||||||
|
style={{ fontSize: `${timerFontSize}vw` }}
|
||||||
|
data-phase={time.phase}
|
||||||
|
>
|
||||||
|
{display}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cx(['secondary', !secondaryContent && 'secondary--hidden'])}
|
||||||
|
style={{ fontSize: `${externalFontSize}vw` }}
|
||||||
|
>
|
||||||
|
{secondaryContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showProgressBar && (
|
||||||
|
<MultiPartProgressBar
|
||||||
|
className={cx(['progress-container', !isPlaying && 'progress-container--paused'])}
|
||||||
|
now={time.current}
|
||||||
|
complete={totalTime}
|
||||||
|
normalColor={viewSettings.normalColor}
|
||||||
|
warning={eventNow?.timeWarning}
|
||||||
|
warningColor={viewSettings.warningColor}
|
||||||
|
danger={eventNow?.timeDanger}
|
||||||
|
dangerColor={viewSettings.dangerColor}
|
||||||
|
hideOvertime={!showFinished}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const isPipSupported = 'documentPictureInPicture' in window;
|
||||||
Reference in New Issue
Block a user