mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 05:13:32 +00:00
Feat/132 (#198)
This commit is contained in:
@@ -14,6 +14,7 @@ import EventSettingsModal from './EventSettingsModal';
|
||||
import IntegrationSettingsModal from './IntegrationSettingsModal';
|
||||
import OscSettingsModal from './OscSettingsModal';
|
||||
import TableOptionsModal from './TableOptionsModal';
|
||||
import ViewsSettingsModal from './ViewsSettingsModal';
|
||||
|
||||
export default function ModalManager(props) {
|
||||
const { isOpen, onClose } = props;
|
||||
@@ -34,6 +35,7 @@ export default function ModalManager(props) {
|
||||
<Tabs size='sm' isLazy>
|
||||
<TabList>
|
||||
<Tab style={{ fontSize: '0.9em' }}>App Settings</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>Viewers</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>Event Data</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>URL Aliases</Tab>
|
||||
<Tab style={{ fontSize: '0.9em' }}>Cuesheet</Tab>
|
||||
@@ -44,6 +46,9 @@ export default function ModalManager(props) {
|
||||
<TabPanel>
|
||||
<AppSettingsModal />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ViewsSettingsModal />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<EventSettingsModal />
|
||||
</TabPanel>
|
||||
|
||||
@@ -96,7 +96,8 @@
|
||||
}
|
||||
|
||||
.submitContainer {
|
||||
margin-top: 2em;
|
||||
margin-top: auto;
|
||||
padding-top: 2em;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ModalBody } from '@chakra-ui/modal';
|
||||
import { FormControl, FormLabel } from '@chakra-ui/react';
|
||||
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
|
||||
|
||||
import { VIEW_SETTINGS } from '../../common/api/apiConstants';
|
||||
import { getView, postView, viewsPlaceholder } from '../../common/api/ontimeApi';
|
||||
import EnableBtn from '../../common/components/buttons/EnableBtn';
|
||||
import { useFetch } from '../../common/hooks/useFetch';
|
||||
import { openLink } from '../../common/utils/linkUtils';
|
||||
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
|
||||
import style from './Modals.module.scss';
|
||||
|
||||
export default function ViewsSettingsModal() {
|
||||
const { data, status, refetch } = useFetch(VIEW_SETTINGS, getView);
|
||||
const [formData, setFormData] = useState(viewsPlaceholder);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
/**
|
||||
* Set formdata from server state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (changed) return;
|
||||
setFormData({ ...data });
|
||||
}, [changed, data]);
|
||||
|
||||
/**
|
||||
* Validate and submit data
|
||||
*/
|
||||
const submitHandler = useCallback(
|
||||
async (event) => {
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
await postView(formData);
|
||||
await refetch();
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
},
|
||||
[formData, refetch]
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverts local state equals to server state
|
||||
*/
|
||||
const revert = useCallback(async () => {
|
||||
setChanged(false);
|
||||
await refetch();
|
||||
}, [refetch]);
|
||||
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {(string | number | boolean)} value - new object parameter value
|
||||
*/
|
||||
const handleChange = useCallback(
|
||||
(field, value) => {
|
||||
const temp = { ...formData };
|
||||
temp[field] = value;
|
||||
setFormData(temp);
|
||||
setChanged(true);
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalBody className={style.modalBody}>
|
||||
<p className={style.notes}>
|
||||
Options related to the viewers
|
||||
<br />
|
||||
🔥 Changes take effect immediately 🔥
|
||||
</p>
|
||||
<form onSubmit={submitHandler}>
|
||||
<div className={style.hSeparator}>Style Options</div>
|
||||
<div className={style.blockNotes}>
|
||||
<span className={style.inlineFlex}>
|
||||
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
|
||||
CSS Style Overrides
|
||||
</span>
|
||||
This feature allows user defined CSS to override the application stylesheets as a way to
|
||||
customise viewers appearance.
|
||||
<br />
|
||||
Read more about it in the documentation{' '}
|
||||
<a
|
||||
href='#!'
|
||||
onClick={() => openLink('https://cpvalente.gitbook.io/ontime/features/custom-styling')}
|
||||
className={style.if}
|
||||
>
|
||||
over at Gitbook
|
||||
</a>
|
||||
</div>
|
||||
<div className={style.modalFields}>
|
||||
<div className={style.modalInline}>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor='overrideStyles'>
|
||||
Override CSS Styles
|
||||
<span className={style.labelNote}>
|
||||
<br />
|
||||
Enable / Disable override
|
||||
</span>
|
||||
</FormLabel>
|
||||
<EnableBtn
|
||||
active={formData.overrideStyles}
|
||||
text={
|
||||
formData.overrideStyles ? 'Style Override Enabled' : 'Style Override Disabled'
|
||||
}
|
||||
actionHandler={() => handleChange('overrideStyles', !formData.overrideStyles)}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<SubmitContainer
|
||||
revert={revert}
|
||||
submitting={submitting}
|
||||
changed={changed}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { EVENT_TABLE, EVENTS_TABLE, VIEW_SETTINGS } from 'common/api/apiConstants';
|
||||
import { fetchEvent } from 'common/api/eventApi';
|
||||
import { fetchAllEvents } from 'common/api/eventsApi';
|
||||
import { useSocket } from 'common/context/socketContext';
|
||||
import { useFetch } from 'common/hooks/useFetch';
|
||||
|
||||
import { EVENT_TABLE, EVENTS_TABLE } from '../../common/api/apiConstants';
|
||||
import { fetchEvent } from '../../common/api/eventApi';
|
||||
import { fetchAllEvents } from '../../common/api/eventsApi';
|
||||
import { useSocket } from '../../common/context/socketContext';
|
||||
import { useFetch } from '../../common/hooks/useFetch';
|
||||
import { getView } from '../../common/api/ontimeApi';
|
||||
|
||||
const withSocket = (Component) => {
|
||||
return (props) => {
|
||||
const { data: eventsData } = useFetch(EVENTS_TABLE, fetchAllEvents);
|
||||
const { data: genData } = useFetch(EVENT_TABLE, fetchEvent);
|
||||
const { data: viewSettings } = useFetch(VIEW_SETTINGS, getView);
|
||||
|
||||
const [publicEvents, setPublicEvents] = useState([]);
|
||||
const [backstageEvents, setBackstageEvents] = useState([]);
|
||||
@@ -176,7 +178,6 @@ const withSocket = (Component) => {
|
||||
setGeneral(genData);
|
||||
}, [genData]);
|
||||
|
||||
|
||||
/********************************************/
|
||||
/*** + titleManager ***/
|
||||
/*** WRAP INFORMATION RELATED TO TITLES ***/
|
||||
@@ -228,6 +229,12 @@ const withSocket = (Component) => {
|
||||
playstate: playback,
|
||||
};
|
||||
|
||||
// prevent render until we get all the data we need
|
||||
if (!viewSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Component.displayName = 'ComponentWithData';
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
@@ -241,6 +248,7 @@ const withSocket = (Component) => {
|
||||
backstageEvents={backstageEvents}
|
||||
selectedId={selectedId}
|
||||
publicSelectedId={publicSelectedId}
|
||||
viewSettings={viewSettings}
|
||||
nextId={nextId}
|
||||
general={general}
|
||||
onAir={onAir}
|
||||
|
||||
+37
-30
@@ -1,25 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import Paginator from 'common/components/views/Paginator';
|
||||
import TitleSide from 'common/components/views/TitleSide';
|
||||
import Paginator from 'common/components/paginator/Paginator';
|
||||
import TitleSide from 'common/components/title-side/TitleSide';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { titleVariants } from '../common/animation';
|
||||
|
||||
import style from './StageManager.module.scss';
|
||||
import './Backstage.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
};
|
||||
|
||||
export default function StageManager(props) {
|
||||
const { publ, title, time, backstageEvents, selectedId, general } = props;
|
||||
export default function Backstage(props) {
|
||||
const { publ, title, time, backstageEvents, selectedId, general, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [filteredEvents, setFilteredEvents] = useState(null);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
@@ -36,6 +39,11 @@ export default function StageManager(props) {
|
||||
setFilteredEvents(f);
|
||||
}, [backstageEvents]);
|
||||
|
||||
// defer rendering until we load stylesheets
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format messages
|
||||
const showPubl = publ.text !== '' && publ.visible;
|
||||
|
||||
@@ -50,15 +58,15 @@ export default function StageManager(props) {
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
return (
|
||||
<div className={style.container__gray}>
|
||||
<div className='backstage'>
|
||||
<NavLogo />
|
||||
|
||||
<div className={style.eventTitle}>{general.title}</div>
|
||||
<div className='event-title'>{general.title}</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{title.showNow && (
|
||||
<motion.div
|
||||
className={style.nowContainer}
|
||||
className='event now'
|
||||
key='now'
|
||||
variants={titleVariants}
|
||||
initial='hidden'
|
||||
@@ -79,7 +87,7 @@ export default function StageManager(props) {
|
||||
<AnimatePresence>
|
||||
{title.showNext && (
|
||||
<motion.div
|
||||
className={style.nextContainer}
|
||||
className='event next'
|
||||
key='next'
|
||||
variants={titleVariants}
|
||||
initial='hidden'
|
||||
@@ -97,15 +105,15 @@ export default function StageManager(props) {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={style.todayContainer}>
|
||||
<div className={style.todayHeaderBlock}>
|
||||
<div className={style.label}>Today</div>
|
||||
<div className={style.nav}>
|
||||
<div className='today-container'>
|
||||
<div className='today-header-block'>
|
||||
<div className='label'>Today</div>
|
||||
<div className='nav'>
|
||||
{pageNumber > 1 &&
|
||||
[...Array(pageNumber).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === currentPage ? style.navItemSelected : style.navItem}
|
||||
className={i === currentPage ? 'nav-item nav-item--selected' : 'nav-item'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -119,27 +127,25 @@ export default function StageManager(props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={showPubl ? style.publicContainer : style.publicContainerHidden}>
|
||||
<div className={style.label}>Public message</div>
|
||||
<div className={style.message}>{publ.text}</div>
|
||||
<div className={showPubl ? 'public-container' : 'public-container public-container--hidden'}>
|
||||
<div className='label'>Public message</div>
|
||||
<div className='message'>{publ.text}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<div className={style.clock}>{clock}</div>
|
||||
<div className='clock-container'>
|
||||
<div className='label'>Time Now</div>
|
||||
<div className='clock'>{clock}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.countdownContainer}>
|
||||
<div className={style.label}>Stage Timer</div>
|
||||
<div className={style.clock}>{stageTimer}</div>
|
||||
<div className='timer-container'>
|
||||
<div className='label'>Stage Timer</div>
|
||||
<div className='timer'>{stageTimer}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.infoContainer}>
|
||||
<div className={style.label}>Info</div>
|
||||
<div className={style.infoMessages}>
|
||||
<div className={style.info}>{general.backstageInfo}</div>
|
||||
</div>
|
||||
<div className={style.qr}>
|
||||
<div className='info'>
|
||||
<div className='label'>Info</div>
|
||||
<div className='info__message'>{general.backstageInfo}</div>
|
||||
<div className='qr'>
|
||||
{general.url != null && general.url !== '' && (
|
||||
<QRCode value={general.url} size={window.innerWidth / 12} level='L' />
|
||||
)}
|
||||
@@ -149,11 +155,12 @@ export default function StageManager(props) {
|
||||
);
|
||||
}
|
||||
|
||||
StageManager.propTypes = {
|
||||
Backstage.propTypes = {
|
||||
publ: PropTypes.object,
|
||||
title: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
backstageEvents: PropTypes.object,
|
||||
selectedId: PropTypes.string,
|
||||
general: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.backstage {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
background: var(--background-color-override, $viewer-background-color);
|
||||
height: 100vh;
|
||||
color: var(--color-override, $viewer-color);
|
||||
font-weight: 300;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 3vw 2fr;
|
||||
grid-template-rows: 15vh 1fr 1fr 0 13vh 13vh;
|
||||
grid-template-areas:
|
||||
' titl titl titl . schd'
|
||||
' now now now . schd'
|
||||
' next next .... . schd'
|
||||
' ... .... .... . ....'
|
||||
' publ publ clck . info'
|
||||
' publ publ time . info';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
|
||||
.event,
|
||||
.public-container,
|
||||
.info,
|
||||
.timer-container,
|
||||
.clock-container,
|
||||
.today-container {
|
||||
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
|
||||
padding: 1vh 1vw;
|
||||
border-radius: 1vw;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
}
|
||||
|
||||
.clock,
|
||||
.timer {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 2.25vw;
|
||||
line-height: 3vw;
|
||||
text-align: center;
|
||||
letter-spacing: 0.25vw;
|
||||
color: var(--color-override, $viewer-color);
|
||||
}
|
||||
}
|
||||
|
||||
.event {
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
margin-left: -1vw;
|
||||
padding: 1vh 2vw;
|
||||
overflow: hidden;
|
||||
|
||||
&.now {
|
||||
grid-area: now;
|
||||
}
|
||||
|
||||
&.next {
|
||||
grid-area: next;
|
||||
}
|
||||
}
|
||||
|
||||
.event-title {
|
||||
grid-area: titl;
|
||||
font-size: 3vw;
|
||||
font-weight: 600;
|
||||
text-decoration: underline 0.5vh;
|
||||
text-decoration-color: var(--accent-color-override, $accent-color);
|
||||
padding-top: 0.2vh;
|
||||
padding-left: 1vw;
|
||||
}
|
||||
|
||||
.today-container {
|
||||
grid-area: schd;
|
||||
padding: 2.5vh 2vw;
|
||||
overflow: hidden;
|
||||
margin-top: 3vh;
|
||||
height: 95%;
|
||||
|
||||
.today-header-block {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5vh;
|
||||
|
||||
.nav {
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-bottom: 2.5vh;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
background-color: $today-item-bg;
|
||||
width: 0.7vw;
|
||||
height: 0.7vw;
|
||||
border-radius: 0.35vw;
|
||||
margin-left: 0.5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.public-container {
|
||||
grid-area: publ;
|
||||
|
||||
&--hidden {
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
}
|
||||
|
||||
.clock-container {
|
||||
grid-area: time;
|
||||
border-radius: 0 0 1vw 1vw;
|
||||
}
|
||||
|
||||
.timer-container {
|
||||
grid-area: clck;
|
||||
border-radius: 1vw 1vw 0 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
grid-area: info;
|
||||
display: grid;
|
||||
padding: 2.5vh 2vw;
|
||||
|
||||
grid-template-rows: 3vh minmax(0, 1fr);
|
||||
grid-template-columns: 3fr 1fr;
|
||||
grid-template-areas:
|
||||
'titl .'
|
||||
'binf qr';
|
||||
gap: 0.5vw;
|
||||
|
||||
&__message {
|
||||
grid-area: binf;
|
||||
font-size: 1.5vw;
|
||||
line-height: 2vw;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qr {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
grid-area: qr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
@use '../../../theme/viewers' as *;
|
||||
|
||||
.container__gray,
|
||||
.container__grayFinished {
|
||||
@include viewer-container;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 3vw 2fr;
|
||||
grid-template-rows: 15vh 1fr 1fr 0 13vh 13vh;
|
||||
grid-template-areas:
|
||||
' titl titl titl . schd'
|
||||
' now now now . schd'
|
||||
' next next .... . schd'
|
||||
' ... .... .... . ....'
|
||||
' publ publ clck . info'
|
||||
' publ publ time . info';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
grid-area: titl;
|
||||
@include viewer-event-title;
|
||||
}
|
||||
|
||||
.nowContainer {
|
||||
grid-area: now;
|
||||
background-color: $bg-gray-950;
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
}
|
||||
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
grid-area: publ;
|
||||
}
|
||||
|
||||
.nextContainer {
|
||||
grid-area: next;
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
}
|
||||
|
||||
.todayContainer {
|
||||
grid-area: schd;
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
grid-area: info;
|
||||
}
|
||||
|
||||
.clockContainer {
|
||||
grid-area: time;
|
||||
border-radius: 0 0 1vw 1vw;
|
||||
}
|
||||
|
||||
.countdownContainer {
|
||||
grid-area: clck;
|
||||
border-radius: 1vw 1vw 0 0;
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import Empty from '../../../common/components/state/Empty';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
||||
import getDelayTo from '../../../common/utils/getDelayTo';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
import { fetchTimerData, sanitiseTitle, timerMessages } from './countdown.helpers';
|
||||
|
||||
import style from './Countdown.module.scss';
|
||||
import './Countdown.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
@@ -18,14 +20,15 @@ const formatOptions = {
|
||||
};
|
||||
|
||||
export default function Countdown(props) {
|
||||
const { backstageEvents, time, selectedId } = props;
|
||||
const { backstageEvents, time, selectedId, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [follow, setFollow] = useState(null);
|
||||
const [runningTimer, setRunningTimer] = useState(0);
|
||||
const [runningMessage, setRunningMessage] = useState('');
|
||||
const [delay, setDelay] = useState(0);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Countdown';
|
||||
}, []);
|
||||
@@ -66,17 +69,15 @@ export default function Countdown(props) {
|
||||
setRunningTimer(timer);
|
||||
}, [follow, selectedId, time]);
|
||||
|
||||
const standby = useMemo(
|
||||
() => time.playstate !== 'start' && selectedId === follow?.id,
|
||||
[follow?.id, selectedId, time.playstate]
|
||||
);
|
||||
// defer rendering until we load stylesheets
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isRunningFinished = useMemo(
|
||||
() => time.finished && runningMessage === timerMessages.running,
|
||||
[time.finished, runningMessage]
|
||||
);
|
||||
|
||||
const isSelected = useMemo(() => runningMessage === timerMessages.running, [runningMessage]);
|
||||
const standby = time.playstate !== 'start' && selectedId === follow?.id;
|
||||
const isRunningFinished = time.finished && runningMessage === timerMessages.running;
|
||||
const isSelected = runningMessage === timerMessages.running;
|
||||
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const startTime =
|
||||
@@ -89,12 +90,12 @@ export default function Countdown(props) {
|
||||
: formatTime(follow.timeEnd + delay, formatOptions);
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<div className='countdown'>
|
||||
<NavLogo />
|
||||
{follow === null ? (
|
||||
<div className={style.eventSelect}>
|
||||
<span className={style.actionTitle}>Select an event to follow</span>
|
||||
<ul className={style.events}>
|
||||
<div className='event-select'>
|
||||
<span className='event-select__title'>Select an event to follow</span>
|
||||
<ul className='event-select__events'>
|
||||
{backstageEvents.length === 0 ? (
|
||||
<Empty dark text='No events in database' />
|
||||
) : (
|
||||
@@ -111,27 +112,29 @@ export default function Countdown(props) {
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className={style.countdownContainer}>
|
||||
<div className={style.timers}>
|
||||
<div className={style.timer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<span className={style.value}>{clock}</span>
|
||||
<div className='countdown-container'>
|
||||
<div className='timer-group'>
|
||||
<div className='aux-timers'>
|
||||
<div className='aux-timers__label'>Time Now</div>
|
||||
<span className='aux-timers__value'>{clock}</span>
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<div className={style.label}>Start Time</div>
|
||||
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>
|
||||
<div className='aux-timers'>
|
||||
<div className='aux-timers__label'>Start Time</div>
|
||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>
|
||||
{startTime}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<div className={style.label}>End Time</div>
|
||||
<span className={`${style.value} ${delay > 0 ? style.delayed : ''}`}>{endTime}</span>
|
||||
<div className='aux-timers'>
|
||||
<div className='aux-timers__label'>End Time</div>
|
||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>
|
||||
{endTime}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.status}>{runningMessage}</div>
|
||||
<div className='status'>{runningMessage}</div>
|
||||
<span
|
||||
className={`${style.countdownClock} ${standby ? style.standby : ''} ${
|
||||
isRunningFinished ? style.finished : ''
|
||||
className={`timer ${standby ? 'timer--paused' : ''} ${
|
||||
isRunningFinished ? 'timer--finished' : ''
|
||||
}`}
|
||||
>
|
||||
{formatDisplay(
|
||||
@@ -139,7 +142,7 @@ export default function Countdown(props) {
|
||||
isSelected || time.waiting
|
||||
)}
|
||||
</span>
|
||||
<div className={style.title}>{follow?.title || 'Untitled Event'}</div>
|
||||
<div className='title'>{follow?.title || 'Untitled Event'}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -150,5 +153,5 @@ Countdown.propTypes = {
|
||||
backstageEvents: PropTypes.array,
|
||||
time: PropTypes.object,
|
||||
selectedId: PropTypes.string,
|
||||
settings: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
|
||||
+22
-22
@@ -1,17 +1,17 @@
|
||||
@use '../../../theme/main' as *;
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.container {
|
||||
.countdown {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
|
||||
background: $bg-black;
|
||||
background: var(--background-color-override, $viewer-background-color);
|
||||
height: 100vh;
|
||||
color: $title-white;
|
||||
color: var(--color-override, $viewer-color);
|
||||
padding: 1vw;
|
||||
|
||||
.eventSelect {
|
||||
.event-select {
|
||||
display: flex;
|
||||
margin-top: 8vh;
|
||||
align-items: center;
|
||||
@@ -19,11 +19,11 @@
|
||||
font-size: max(1.3vw, 12px);
|
||||
flex-direction: column;
|
||||
|
||||
.actionTitle {
|
||||
&__title {
|
||||
font-size: max(2vw, 14px);
|
||||
}
|
||||
|
||||
.events {
|
||||
&__events {
|
||||
margin-top: 1em;
|
||||
overflow-y: auto;
|
||||
height: 70vh;
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.countdownContainer {
|
||||
.countdown-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
@@ -47,28 +47,27 @@
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
.timers {
|
||||
.timer-group {
|
||||
grid-area: timers;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-end;
|
||||
|
||||
.timer {
|
||||
.aux-timers {
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
&__label {
|
||||
font-size: max(1.3vw, 12px);
|
||||
color: $ontime-pink;
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
}
|
||||
|
||||
.value {
|
||||
&__value {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: max(2vw, 14px);
|
||||
letter-spacing: 0.3px;
|
||||
color: #ddd;
|
||||
|
||||
&.delayed {
|
||||
color: $block-delay-color;
|
||||
&--delayed {
|
||||
color: $delay-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,8 +77,8 @@
|
||||
grid-area: title;
|
||||
font-size: max(4vw, 18px);
|
||||
align-self: center;
|
||||
color: $ontime-pink;
|
||||
background-color: $bg-gray-1000;
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
|
||||
padding: 1vh 2vw;
|
||||
border-radius: 1vw;
|
||||
}
|
||||
@@ -92,7 +91,7 @@
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.countdownClock {
|
||||
.timer {
|
||||
grid-area: clock;
|
||||
line-height: 20vw;
|
||||
font-size: 21vw;
|
||||
@@ -101,12 +100,13 @@
|
||||
align-self: flex-start;
|
||||
opacity: 1.0;
|
||||
transition: opacity 0.5s;
|
||||
color: var(--timer-color-override, $viewer-color);
|
||||
|
||||
&.standby {
|
||||
&--paused {
|
||||
opacity: 0.6;
|
||||
}
|
||||
&.finished {
|
||||
color: $ontime-pink-variant;
|
||||
&--finished {
|
||||
color: $timer-finished-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
@use '../../../theme/viewers' as *;
|
||||
|
||||
.container__gray,
|
||||
.container__grayFinished {
|
||||
@include viewer-container;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 3vw 2fr;
|
||||
grid-template-rows: 15vh 1fr 1fr 0 13vh 13vh;
|
||||
grid-template-areas:
|
||||
' titl titl titl . schd'
|
||||
' now now now . schd'
|
||||
' next next .... . schd'
|
||||
' ... .... .... . ....'
|
||||
' publ publ .... . info'
|
||||
' publ publ time . info';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
grid-area: titl;
|
||||
@include viewer-event-title;
|
||||
}
|
||||
|
||||
.nowContainer {
|
||||
grid-area: now;
|
||||
background-color: $bg-gray-950;
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
}
|
||||
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
grid-area: publ;
|
||||
}
|
||||
|
||||
.nextContainer {
|
||||
grid-area: next;
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
}
|
||||
|
||||
.todayContainer {
|
||||
grid-area: schd;
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
grid-area: info;
|
||||
}
|
||||
|
||||
.clockContainer {
|
||||
grid-area: time;
|
||||
border-radius: 1vw;
|
||||
}
|
||||
+8
-8
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import NavLogo from "../../../../common/components/nav/NavLogo";
|
||||
import NavLogo from "../../../common/components/nav/NavLogo";
|
||||
|
||||
import style from './LowerClean.module.css';
|
||||
import './LowerClean.scss';
|
||||
|
||||
export default function LowerClean(props) {
|
||||
const { lower, title, options } = props;
|
||||
@@ -79,7 +79,7 @@ export default function LowerClean(props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={style.lowerThird}
|
||||
className='lower-third clean'
|
||||
style={{
|
||||
backgroundColor: options.keyColour || defaults.keyColour,
|
||||
color: options.textColour || defaults.textColour,
|
||||
@@ -92,7 +92,7 @@ export default function LowerClean(props) {
|
||||
<AnimatePresence>
|
||||
{showLower && (
|
||||
<motion.div
|
||||
className={style.lowerContainer}
|
||||
className='lower-container'
|
||||
style={{
|
||||
backgroundColor: options.bgColour || defaults.bgColour,
|
||||
top: options.posY || defaults.posY,
|
||||
@@ -103,10 +103,10 @@ export default function LowerClean(props) {
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
>
|
||||
<motion.div className={style.title} variants={titleVariants}>
|
||||
<motion.div className='title' variants={titleVariants}>
|
||||
{title.titleNow}
|
||||
</motion.div>
|
||||
<motion.div className={style.subtitle} variants={titleVariants}>
|
||||
<motion.div className='subtitle' variants={titleVariants}>
|
||||
{title.presenterNow}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
@@ -116,7 +116,7 @@ export default function LowerClean(props) {
|
||||
<AnimatePresence>
|
||||
{showLowerMessage && (
|
||||
<motion.div
|
||||
className={style.messageContainer}
|
||||
className='message-container'
|
||||
style={{
|
||||
backgroundColor: options.bgColour || defaults.bgColour,
|
||||
}}
|
||||
@@ -126,7 +126,7 @@ export default function LowerClean(props) {
|
||||
exit={{ scaleY: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className={style.message}>{lower.text}</div>
|
||||
<div className='message'>{lower.text}</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -0,0 +1,26 @@
|
||||
.lower-third.clean {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
height: 100%;
|
||||
color: #fffffa;
|
||||
font-size: 4vh;
|
||||
|
||||
.lower-container {
|
||||
position: absolute;
|
||||
padding: 1vh 2vh;
|
||||
border-radius: 1vh;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
position: absolute;
|
||||
bottom: 2vh;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
.message {
|
||||
font-size: 3.5vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import NavLogo from "../../../../common/components/nav/NavLogo";
|
||||
import NavLogo from "../../../common/components/nav/NavLogo";
|
||||
|
||||
import style from './LowerLines.module.css';
|
||||
import './LowerLines.scss';
|
||||
|
||||
export default function LowerLines(props) {
|
||||
const { lower, title, options } = props;
|
||||
@@ -122,7 +122,7 @@ export default function LowerLines(props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={style.lowerThird}
|
||||
className='lower-third lines'
|
||||
style={{
|
||||
backgroundColor: options.keyColour || defaults.keyColour,
|
||||
color: options.textColour || defaults.textColour,
|
||||
@@ -135,7 +135,7 @@ export default function LowerLines(props) {
|
||||
<AnimatePresence>
|
||||
{showLower && (
|
||||
<motion.div
|
||||
className={style.lowerContainer}
|
||||
className='lower-container'
|
||||
style={{ backgroundColor: options.bgColour || defaults.bgColour }}
|
||||
variants={lowerThirdVariants}
|
||||
initial='hidden'
|
||||
@@ -143,21 +143,21 @@ export default function LowerLines(props) {
|
||||
exit='exit'
|
||||
>
|
||||
<motion.div
|
||||
className={style.titleContainer}
|
||||
className='title-container'
|
||||
variants={titleContainerVariants}
|
||||
>
|
||||
<motion.div className={style.title} variants={titleVariants}>
|
||||
<motion.div className='title' variants={titleVariants}>
|
||||
{title.titleNow}
|
||||
</motion.div>
|
||||
<div className={style.titleDecor} />
|
||||
<div className='title-decor' />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className={style.subtitleContainer}
|
||||
className='subtitle-container'
|
||||
variants={subtitleContainerVariants}
|
||||
>
|
||||
<div className={style.subDecor} />
|
||||
<div className='sub-decor' />
|
||||
<motion.div
|
||||
className={style.subtitle}
|
||||
className='subtitle'
|
||||
variants={subtitleVariants}
|
||||
>
|
||||
{title.presenterNow}
|
||||
@@ -170,14 +170,14 @@ export default function LowerLines(props) {
|
||||
<AnimatePresence>
|
||||
{showLowerMessage && (
|
||||
<motion.div
|
||||
className={style.messageContainer}
|
||||
className='message-container'
|
||||
key='modal'
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ scaleY: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className={style.message}>{lower.text}</div>
|
||||
<div className='message'>{lower.text}</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -0,0 +1,66 @@
|
||||
.lower-third.lines {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
height: 100%;
|
||||
color: #fffffa;
|
||||
font-size: 4vh;
|
||||
|
||||
.lower-container {
|
||||
position: absolute;
|
||||
top: 75vh;
|
||||
background-color: #0003;
|
||||
padding: 1vh 1vw 1vh 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 45vw;
|
||||
border-radius: 0 1vh 1vh 0;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
position: absolute;
|
||||
bottom: 2vh;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: #0003;
|
||||
|
||||
.message {
|
||||
font-size: 3.5vh;
|
||||
}
|
||||
}
|
||||
|
||||
.title-container,
|
||||
.subtitle-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
grid-template-areas: 'decor text';
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-bottom: 1vh;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.title-decor,
|
||||
.sub-decor {
|
||||
grid-area: decor;
|
||||
height: 4vh;
|
||||
width: 100%;
|
||||
background-color: #ff6969;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgb(255 105 105 / 100%) 0%,
|
||||
rgb(255 132 132 / 100%) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle {
|
||||
grid-area: text;
|
||||
padding-left: 1vw;
|
||||
justify-self: right;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
|
||||
import LowerClean from './LowerClean';
|
||||
import LowerLines from './LowerLines';
|
||||
|
||||
const isEqual = require('react-fast-compare');
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return isEqual(prevProps.title, nextProps.title) && isEqual(prevProps.lower, nextProps.lower);
|
||||
};
|
||||
|
||||
const Lower = (props) => {
|
||||
const { title, lower, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [titles, setTitles] = useState({
|
||||
titleNow: '',
|
||||
titleNext: '',
|
||||
subtitleNow: '',
|
||||
subtitleNext: '',
|
||||
presenterNow: '',
|
||||
presenterNext: '',
|
||||
showNow: false,
|
||||
showNext: false,
|
||||
});
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Lower Thirds';
|
||||
}, []);
|
||||
|
||||
// reload if data changes
|
||||
useEffect(() => {
|
||||
// clear titles if necessary
|
||||
// will trigger an animation out in the component
|
||||
let timeout = null;
|
||||
if (
|
||||
title?.titleNow !== titles?.titleNow ||
|
||||
title?.subtitleNow !== titles?.subtitleNow ||
|
||||
title?.presenterNow !== titles?.presenterNow
|
||||
) {
|
||||
setTitles((t) => ({ ...t, showNow: false }));
|
||||
|
||||
const transitionTime = 2000;
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
setTitles(title);
|
||||
}, transitionTime);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout != null) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, [title.titleNow, title.subtitleNow, title.presenterNow]);
|
||||
|
||||
// defer rendering until we load stylesheets
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: sanitize data
|
||||
// getting config from URL: preset, size, transition, bg, text, key
|
||||
// eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5
|
||||
// Check for user options
|
||||
// create aux
|
||||
const options = {};
|
||||
|
||||
// preset: selector
|
||||
// Should be a number 1-n
|
||||
const p = parseInt(searchParams.get('preset'), 10);
|
||||
const preset = !isNaN(p) ? 1 : p;
|
||||
|
||||
// size: multiplier
|
||||
// Should be a number 0.0-n
|
||||
const s = searchParams.get('size');
|
||||
if (s) options.size = s;
|
||||
|
||||
// transitionIn: seconds
|
||||
// Should be a number 0-n
|
||||
const t = parseInt(searchParams.get('transition'), 10);
|
||||
if (!isNaN(t)) options.transitionIn = t;
|
||||
|
||||
// textColour: string
|
||||
// Should be a hex string '#ffffff'
|
||||
const c = searchParams.get('text');
|
||||
if (c) options.textColour = `#${c}`;
|
||||
|
||||
// bgColour: string
|
||||
// Should be a hex string '#ffffff'
|
||||
const b = searchParams.get('bg');
|
||||
if (b) options.bgColour = `#${b}`;
|
||||
|
||||
// key: string
|
||||
// Should be a hex string '#00FF00' with key colour
|
||||
const k = searchParams.get('key');
|
||||
if (k) options.keyColour = `#${k}`;
|
||||
|
||||
// fadeOut: seconds
|
||||
// Should be a number 0-n
|
||||
const f = parseInt(searchParams.get('fadeout'), 10);
|
||||
if (!isNaN(f)) options.fadeOut = f;
|
||||
|
||||
// x: pixels
|
||||
// Should be a number 0-n
|
||||
const x = parseInt(searchParams.get('x'), 10);
|
||||
if (!isNaN(x)) options.posX = x;
|
||||
|
||||
// y: pixels
|
||||
// Should be a number 0-n
|
||||
const y = parseInt(searchParams.get('y'), 10);
|
||||
if (!isNaN(y)) options.posY = y;
|
||||
|
||||
switch (preset) {
|
||||
case 0:
|
||||
return <LowerClean lower={lower} title={titles} options={options} />;
|
||||
case 1:
|
||||
return <LowerLines lower={lower} title={titles} options={options} />;
|
||||
default:
|
||||
return <LowerLines lower={lower} title={titles} options={options} />;
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(Lower, areEqual);
|
||||
|
||||
Lower.propTypes = {
|
||||
title: PropTypes.object,
|
||||
lower: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
+24
-8
@@ -1,16 +1,27 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||
|
||||
import style from './MinimalTimer.module.scss';
|
||||
import './MinimalTimer.scss';
|
||||
|
||||
export default function MinimalTimer(props) {
|
||||
const { pres, time } = props;
|
||||
const { pres, time, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
document.title = 'ontime - Minimal Timer';
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Minimal Timer';
|
||||
}, []);
|
||||
|
||||
// defer rendering until we load stylesheets
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get config from url: key, text, font, size, hidenav, hideovertime
|
||||
// eg. http://localhost:3000/minimal?key=f00&text=fff
|
||||
@@ -102,11 +113,11 @@ export default function MinimalTimer(props) {
|
||||
const isPlaying = time.playstate !== 'pause';
|
||||
const timer = formatDisplay(time.running, true);
|
||||
const clean = timer.replace('/:/g', '');
|
||||
const finishedStyle = userOptions?.hideOvertime ? style.container : style.containerFinished;
|
||||
const showFinished = time.isNegative && !userOptions?.hideOvertime;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={time.finished ? finishedStyle : style.container}
|
||||
className={showFinished ? 'minimal-timer minimal-timer--finished' : 'minimal-timer'}
|
||||
style={{
|
||||
backgroundColor: userOptions.keyColour,
|
||||
color: userOptions.textColour,
|
||||
@@ -116,13 +127,17 @@ export default function MinimalTimer(props) {
|
||||
data-testid='minimal-timer'
|
||||
>
|
||||
{!hideMessagesOverlay && (
|
||||
<div className={showOverlay ? style.messageOverlayActive : style.messageOverlay}>
|
||||
<div className={style.message}>{pres.text}</div>
|
||||
<div
|
||||
className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}
|
||||
>
|
||||
<div className='message'>{pres.text}</div>
|
||||
</div>
|
||||
)}
|
||||
{!userOptions?.hideNav && <NavLogo />}
|
||||
<div
|
||||
className={isPlaying ? style.timer : style.timerPaused}
|
||||
className={`timer ${!isPlaying ? 'timer--paused' : ''} ${
|
||||
showFinished ? 'timer--finished' : ''
|
||||
}`}
|
||||
style={{
|
||||
fontSize: `${(89 / (clean.length - 1)) * userOptions.size}vw`,
|
||||
fontFamily: userOptions.font,
|
||||
@@ -139,4 +154,5 @@ export default function MinimalTimer(props) {
|
||||
MinimalTimer.propTypes = {
|
||||
pres: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.minimal-timer {
|
||||
background: var(--background-color-override, $viewer-background-color);
|
||||
height: 100vh;
|
||||
color: var(--color-override, $viewer-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1vw;
|
||||
border: 1vw solid transparent;
|
||||
|
||||
&--finished {
|
||||
border: 1vw solid $timer-finished-color;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-family: "Arial Black", sans-serif;
|
||||
font-size: 20vw;
|
||||
position: relative;
|
||||
color: var(--timer-color-override, $viewer-color);
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
|
||||
&--paused {
|
||||
opacity: 0.6;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
&--finished {
|
||||
color: $timer-finished-color;
|
||||
}
|
||||
}
|
||||
|
||||
/* =================== OVERLAY ===================*/
|
||||
|
||||
.message-overlay {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: $viewer-overlay-bg-color;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
|
||||
&--active {
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
width: inherit;
|
||||
padding: 2vw;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
color: white;
|
||||
transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
font-size: 15vw;
|
||||
line-height: 30vh;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
+33
-35
@@ -1,15 +1,17 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import Paginator from 'common/components/views/Paginator';
|
||||
import Paginator from 'common/components/paginator/Paginator';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
import style from './Pip.module.scss';
|
||||
import './Pip.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
@@ -17,20 +19,13 @@ const formatOptions = {
|
||||
};
|
||||
|
||||
export default function Pip(props) {
|
||||
const { time, backstageEvents, selectedId, general } = props;
|
||||
const [size, setSize] = useState('');
|
||||
const pipAreaRef = useRef(null);
|
||||
const { time, backstageEvents, selectedId, general, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const ref = useRef(null);
|
||||
const [filteredEvents, setFilteredEvents] = useState(null);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
|
||||
// calculate pip size
|
||||
useLayoutEffect(() => {
|
||||
const h = pipAreaRef.current.clientHeight;
|
||||
const w = pipAreaRef.current.clientWidth;
|
||||
setSize(`${w} x ${h}`);
|
||||
}, []);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Pip';
|
||||
@@ -57,6 +52,11 @@ export default function Pip(props) {
|
||||
setFilteredEvents(events.filter((e) => e.type === 'event'));
|
||||
}, [backstageEvents]);
|
||||
|
||||
// defer rendering until we load stylesheets
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format messages
|
||||
const showInfo = general.backstageInfo !== '' && general.backstageInfo != null;
|
||||
let stageTimer = formatDisplay(Math.abs(time.running), true);
|
||||
@@ -65,20 +65,20 @@ export default function Pip(props) {
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
return (
|
||||
<div className={style.container__gray}>
|
||||
<div className='pip'>
|
||||
<NavLogo />
|
||||
|
||||
<div className={style.eventTitle}>{general.title}</div>
|
||||
<div className='event-title'>{general.title}</div>
|
||||
|
||||
<div className={style.todayContainer}>
|
||||
<div className={style.todayHeaderBlock}>
|
||||
<div className={style.label}>Today</div>
|
||||
<div className={style.nav}>
|
||||
<div className='today-container'>
|
||||
<div className='today-header-block'>
|
||||
<div className='label'>Today</div>
|
||||
<div className='nav'>
|
||||
{pageNumber > 1 &&
|
||||
[...Array(pageNumber).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === currentPage ? style.navItemSelected : style.navItem}
|
||||
className={i === currentPage ? 'nav-item nav-item--selected' : 'nav-item'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -94,19 +94,16 @@ export default function Pip(props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style.pip} ref={pipAreaRef}>
|
||||
<Emptyimage className={style.empty} />
|
||||
<span className={style.piptext}>{size}</span>
|
||||
<div className='pip-placeholder' ref={ref}>
|
||||
<Emptyimage className='pip-placeholder__empty' />
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{showInfo && (
|
||||
<motion.div className={style.infoContainer}>
|
||||
<div className={style.label}>Info</div>
|
||||
<div className={style.infoMessages}>
|
||||
<div className={style.info}>{general.backstageInfo}</div>
|
||||
</div>
|
||||
<div className={style.qr}>
|
||||
<motion.div className='info-container'>
|
||||
<div className='label'>Info</div>
|
||||
<div className='info-message'>{general.backstageInfo}</div>
|
||||
<div className='qr'>
|
||||
{general.url != null && general.url !== '' && (
|
||||
<QRCode value={general.url} size={window.innerWidth / 12} level='L' />
|
||||
)}
|
||||
@@ -115,14 +112,14 @@ export default function Pip(props) {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<div className={style.clock}>{clock}</div>
|
||||
<div className='clock-container'>
|
||||
<div className='label'>Time Now</div>
|
||||
<div className='clock'>{clock}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.countdownContainer}>
|
||||
<div className={style.label}>Stage Timer</div>
|
||||
<div className={style.clock}>{stageTimer}</div>
|
||||
<div className='timer-container'>
|
||||
<div className='label'>Stage Timer</div>
|
||||
<div className='timer'>{stageTimer}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -133,4 +130,5 @@ Pip.propTypes = {
|
||||
backstageEvents: PropTypes.object,
|
||||
selectedId: PropTypes.string,
|
||||
general: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.pip {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
background: var(--background-color-override, $viewer-background-color);
|
||||
height: 100vh;
|
||||
color: var(--color-override, $viewer-color);
|
||||
font-weight: 300;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 20vw 20vw 18vw 3vw 1fr;
|
||||
grid-template-rows: 60vh 1fr 1fr 1fr;
|
||||
grid-template-areas:
|
||||
' pip pip pip . schd'
|
||||
' titl titl titl . schd'
|
||||
' info info clck . schd'
|
||||
' info info time . schd';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
|
||||
.today-container,
|
||||
.info-container,
|
||||
.clock-container,
|
||||
.timer-container {
|
||||
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
|
||||
padding: 1vh 1vw;
|
||||
border-radius: 1vw;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
}
|
||||
|
||||
.clock,
|
||||
.timer {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 2.25vw;
|
||||
line-height: 3vw;
|
||||
text-align: center;
|
||||
letter-spacing: 0.25vw;
|
||||
color: var(--color-override, $viewer-color);
|
||||
}
|
||||
}
|
||||
|
||||
.event-title {
|
||||
grid-area: titl;
|
||||
font-size: 3vw;
|
||||
font-weight: 600;
|
||||
text-decoration: underline 0.5vh;
|
||||
text-decoration-color: var(--accent-color-override, $accent-color);
|
||||
padding-top: 0.2vh;
|
||||
padding-left: 1vw;
|
||||
}
|
||||
|
||||
.today-container {
|
||||
grid-area: schd;
|
||||
padding: 2.5vh 2vw;
|
||||
overflow: hidden;
|
||||
margin-top: 3vh;
|
||||
height: 95%;
|
||||
|
||||
.today-header-block {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5vh;
|
||||
|
||||
.nav {
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-bottom: 2.5vh;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
background-color: $today-item-bg;
|
||||
width: 0.7vw;
|
||||
height: 0.7vw;
|
||||
border-radius: 0.35vw;
|
||||
margin-left: 0.5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pip-placeholder {
|
||||
grid-area: pip;
|
||||
background-color: $pip-bg-color;
|
||||
border: 1px solid $pip-border-color;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
&__empty {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.info-container {
|
||||
grid-area: info;
|
||||
display: grid;
|
||||
|
||||
grid-template-rows: 3vh minmax(0, 1fr);
|
||||
grid-template-columns: 3fr 1fr;
|
||||
grid-template-areas:
|
||||
'titl .'
|
||||
'binf qr';
|
||||
gap: 0.5vw;
|
||||
|
||||
.info-message {
|
||||
grid-area: binf;
|
||||
font-size: 1.5vw;
|
||||
line-height: 2vw;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qr {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
grid-area: qr;
|
||||
}
|
||||
}
|
||||
|
||||
.clock-container {
|
||||
grid-area: time;
|
||||
border-radius: 0 0 1vw 1vw;
|
||||
}
|
||||
|
||||
.timer-container {
|
||||
grid-area: clck;
|
||||
border-radius: 1vw 1vw 0 0;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
@use '../../../theme/viewers' as *;
|
||||
|
||||
.container__gray,
|
||||
.container__grayFinished {
|
||||
@include viewer-container;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 20vw 20vw 18vw 3vw 1fr;
|
||||
grid-template-rows: 60vh 1fr 1fr 1fr;
|
||||
grid-template-areas:
|
||||
' pip pip pip . schd'
|
||||
' titl titl titl . schd'
|
||||
' info info clck . schd'
|
||||
' info info time . schd';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
grid-area: titl;
|
||||
@include viewer-event-title;
|
||||
}
|
||||
|
||||
.pip {
|
||||
grid-area: pip;
|
||||
background-color: $bg-black-100;
|
||||
border: 1px solid $bg-black-200;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.empty {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.piptext {
|
||||
color: $bg-gray-900;
|
||||
font-weight: 600;
|
||||
font-size: 4vh;
|
||||
}
|
||||
|
||||
.infoContainer,
|
||||
.clockContainer,
|
||||
.countdownContainer {
|
||||
background-color: $bg-gray-1000;
|
||||
padding: 1vh 1vw;
|
||||
}
|
||||
|
||||
.todayContainer {
|
||||
grid-area: schd;
|
||||
}
|
||||
|
||||
.countdownContainer {
|
||||
grid-area: clck;
|
||||
border-radius: 1vw 1vw 0 0;
|
||||
}
|
||||
|
||||
.clockContainer {
|
||||
border-radius: 0 0 1vw 1vw;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
.lowerThird {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
height: 100%;
|
||||
|
||||
color: #fffffa;
|
||||
font-size: 4vh;
|
||||
}
|
||||
|
||||
.lowerContainer {
|
||||
position: absolute;
|
||||
padding: 1vh 2vh;
|
||||
border-radius: 1vh;
|
||||
}
|
||||
|
||||
.messageContainer {
|
||||
position: absolute;
|
||||
bottom: 2vh;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 3.5vh;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
.lowerThird {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
height: 100%;
|
||||
|
||||
color: #fffffa;
|
||||
font-size: 4vh;
|
||||
}
|
||||
|
||||
.lowerContainer {
|
||||
position: absolute;
|
||||
top: 75vh;
|
||||
background-color: #00000033;
|
||||
|
||||
padding: 1vh 1vw 1vh 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 45vw;
|
||||
border-radius: 0 1vh 1vh 0;
|
||||
}
|
||||
|
||||
.messageContainer {
|
||||
position: absolute;
|
||||
bottom: 2vh;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.titleContainer,
|
||||
.subtitleContainer {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
grid-template-areas: 'decor text';
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.titleContainer,
|
||||
.subtitleContainer {
|
||||
margin-bottom: 1vh;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.titleDecor,
|
||||
.subDecor {
|
||||
grid-area: decor;
|
||||
height: 4vh;
|
||||
background-color: #ff6969;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 105, 105, 1) 0%,
|
||||
rgba(255, 132, 132, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.titleDecor,
|
||||
.subDecor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle {
|
||||
grid-area: text;
|
||||
padding-left: 1vw;
|
||||
justify-self: right;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.messageContainer {
|
||||
background-color: #00000033;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 3.5vh;
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import isEqual from "react-fast-compare";
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import LowerClean from './LowerClean';
|
||||
import LowerLines from './LowerLines';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
isEqual(prevProps.title, nextProps.title) &&
|
||||
isEqual(prevProps.lower, nextProps.lower)
|
||||
);
|
||||
};
|
||||
|
||||
const Lower = (props) => {
|
||||
const { title, lower } = props;
|
||||
const [searchParams,] = useSearchParams();
|
||||
const [titles, setTitles] = useState({
|
||||
titleNow: '',
|
||||
titleNext: '',
|
||||
subtitleNow: '',
|
||||
subtitleNext: '',
|
||||
presenterNow: '',
|
||||
presenterNext: '',
|
||||
showNow: false,
|
||||
showNext: false,
|
||||
});
|
||||
const [preset, setPreset] = useState(1);
|
||||
const [lowerOptions, setLowerOptions] = useState({});
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Lower Thirds';
|
||||
}, []);
|
||||
|
||||
// reload if data changes
|
||||
useEffect(() => {
|
||||
// clear titles if necessary
|
||||
// will trigger an animation out in the component
|
||||
let timeout = null;
|
||||
if (
|
||||
title?.titleNow !== titles?.titleNow ||
|
||||
title?.subtitleNow !== titles?.subtitleNow ||
|
||||
title?.presenterNow !== titles?.presenterNow
|
||||
) {
|
||||
setTitles((t) => ({ ...t, showNow: false }));
|
||||
|
||||
const transitionTime = 2000;
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
setTitles(title);
|
||||
}, transitionTime);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout != null) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, [title.titleNow, title.subtitleNow, title.presenterNow]);
|
||||
|
||||
// TODO: sanitize data
|
||||
// getting config from URL: preset, size, transition, bg, text, key
|
||||
// eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5
|
||||
// Check for user options
|
||||
useEffect(() => {
|
||||
// create aux
|
||||
const options = {};
|
||||
|
||||
// preset: selector
|
||||
// Should be a number 1-n
|
||||
const p = parseInt(searchParams.get('preset'), 10);
|
||||
if (!isNaN(p)) setPreset(p);
|
||||
|
||||
// size: multiplier
|
||||
// Should be a number 0.0-n
|
||||
const s = searchParams.get('size');
|
||||
if (s) options.size = s;
|
||||
|
||||
// transitionIn: seconds
|
||||
// Should be a number 0-n
|
||||
const t = parseInt(searchParams.get('transition'), 10);
|
||||
if (!isNaN(t)) options.transitionIn = t;
|
||||
|
||||
// textColour: string
|
||||
// Should be a hex string '#ffffff'
|
||||
const c = searchParams.get('text');
|
||||
if (c) options.textColour = `#${c}`;
|
||||
|
||||
// bgColour: string
|
||||
// Should be a hex string '#ffffff'
|
||||
const b = searchParams.get('bg');
|
||||
if (b) options.bgColour = `#${b}`;
|
||||
|
||||
// key: string
|
||||
// Should be a hex string '#00FF00' with key colour
|
||||
const k = searchParams.get('key');
|
||||
if (k) options.keyColour = `#${k}`;
|
||||
|
||||
// fadeOut: seconds
|
||||
// Should be a number 0-n
|
||||
const f = parseInt(searchParams.get('fadeout'), 10);
|
||||
if (!isNaN(f)) options.fadeOut = f;
|
||||
|
||||
// x: pixels
|
||||
// Should be a number 0-n
|
||||
const x = parseInt(searchParams.get('x'), 10);
|
||||
if (!isNaN(x)) options.posX = x;
|
||||
|
||||
// y: pixels
|
||||
// Should be a number 0-n
|
||||
const y = parseInt(searchParams.get('y'), 10);
|
||||
if (!isNaN(y)) options.posY = y;
|
||||
|
||||
setLowerOptions({
|
||||
...options,
|
||||
set: true,
|
||||
});
|
||||
}, [searchParams]);
|
||||
|
||||
// Defer rendering until we have data ready
|
||||
if (!lowerOptions.set) return null;
|
||||
|
||||
switch (preset) {
|
||||
case 0:
|
||||
return (
|
||||
<LowerClean lower={lower} title={titles} options={lowerOptions} />
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<LowerLines lower={lower} title={titles} options={lowerOptions} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<LowerLines lower={lower} title={titles} options={lowerOptions} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(Lower, areEqual);
|
||||
+32
-25
@@ -1,15 +1,17 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import Paginator from 'common/components/views/Paginator';
|
||||
import TitleSide from 'common/components/views/TitleSide';
|
||||
import Paginator from 'common/components/paginator/Paginator';
|
||||
import TitleSide from 'common/components/title-side/TitleSide';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { titleVariants } from '../common/animation';
|
||||
|
||||
import style from './Public.module.scss';
|
||||
import './Public.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
@@ -17,7 +19,8 @@ const formatOptions = {
|
||||
};
|
||||
|
||||
export default function Public(props) {
|
||||
const { publ, publicTitle, time, events, publicSelectedId, general } = props;
|
||||
const { publ, publicTitle, time, events, publicSelectedId, general, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
|
||||
@@ -26,21 +29,26 @@ export default function Public(props) {
|
||||
document.title = 'ontime - Public Screen';
|
||||
}, []);
|
||||
|
||||
// defer rendering until we load stylesheets
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format messages
|
||||
const showPubl = publ.text !== '' && publ.visible;
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
return (
|
||||
<div className={style.container__gray}>
|
||||
<div className='public-screen'>
|
||||
<NavLogo />
|
||||
|
||||
<div className={style.eventTitle}>{general.title}</div>
|
||||
<div className='event-title'>{general.title}</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{publicTitle.showNow && (
|
||||
<motion.div
|
||||
className={style.nowContainer}
|
||||
className='event now'
|
||||
key='now'
|
||||
variants={titleVariants}
|
||||
initial='hidden'
|
||||
@@ -61,7 +69,7 @@ export default function Public(props) {
|
||||
<AnimatePresence>
|
||||
{publicTitle.showNext && (
|
||||
<motion.div
|
||||
className={style.nextContainer}
|
||||
className='event next'
|
||||
key='next'
|
||||
variants={titleVariants}
|
||||
initial='hidden'
|
||||
@@ -79,15 +87,15 @@ export default function Public(props) {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={style.todayContainer}>
|
||||
<div className={style.todayHeaderBlock}>
|
||||
<div className={style.label}>Today</div>
|
||||
<div className={style.nav}>
|
||||
<div className='today-container'>
|
||||
<div className='today-header-block'>
|
||||
<div className='label'>Today</div>
|
||||
<div className='nav'>
|
||||
{pageNumber > 1 &&
|
||||
[...Array(pageNumber).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === currentPage ? style.navItemSelected : style.navItem}
|
||||
className={i === currentPage ? 'nav-item nav-item--selected' : 'nav-item'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -101,22 +109,20 @@ export default function Public(props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={showPubl ? style.publicContainer : style.publicContainerHidden}>
|
||||
<div className={style.label}>Public message</div>
|
||||
<div className={style.message}>{publ.text}</div>
|
||||
<div className={showPubl ? 'public-container' : 'public-container public-container--hidden'}>
|
||||
<div className='label'>Public message</div>
|
||||
<div className='message'>{publ.text}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<div className={style.clock}>{clock}</div>
|
||||
<div className='clock-container'>
|
||||
<div className='label'>Time Now</div>
|
||||
<div className='clock'>{clock}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.infoContainer}>
|
||||
<div className={style.label}>Info</div>
|
||||
<div className={style.infoMessages}>
|
||||
<div className={style.info}>{general.publicInfo}</div>
|
||||
</div>
|
||||
<div className={style.qr}>
|
||||
<div className='info'>
|
||||
<div className='label'>Info</div>
|
||||
<div className='info__message'>{general.publicInfo}</div>
|
||||
<div className='qr'>
|
||||
{general.url != null && general.url !== '' && (
|
||||
<QRCode value={general.url} size={window.innerWidth / 12} level='L' />
|
||||
)}
|
||||
@@ -133,4 +139,5 @@ Public.propTypes = {
|
||||
events: PropTypes.object,
|
||||
publicSelectedId: PropTypes.string,
|
||||
general: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.public-screen {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
background: var(--background-color-override, $viewer-background-color);
|
||||
height: 100vh;
|
||||
color: var(--color-override, $viewer-color);
|
||||
font-weight: 300;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 3vw 2fr;
|
||||
grid-template-rows: 15vh 1fr 1fr 0 13vh 13vh;
|
||||
grid-template-areas:
|
||||
' titl titl titl . schd'
|
||||
' now now now . schd'
|
||||
' next next .... . schd'
|
||||
' ... .... .... . ....'
|
||||
' publ publ .... . info'
|
||||
' publ publ time . info';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
|
||||
.event,
|
||||
.public-container,
|
||||
.info,
|
||||
.timer-container,
|
||||
.clock-container,
|
||||
.today-container {
|
||||
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
|
||||
padding: 1vh 1vw;
|
||||
border-radius: 1vw;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
}
|
||||
|
||||
.clock,
|
||||
.timer {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 2.25vw;
|
||||
line-height: 3vw;
|
||||
text-align: center;
|
||||
letter-spacing: 0.25vw;
|
||||
color: var(--color-override, $viewer-color);
|
||||
}
|
||||
}
|
||||
|
||||
.event {
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
margin-left: -1vw;
|
||||
padding: 1vh 2vw;
|
||||
overflow: hidden;
|
||||
|
||||
&.now {
|
||||
grid-area: now;
|
||||
}
|
||||
&.next {
|
||||
grid-area: next;
|
||||
}
|
||||
}
|
||||
|
||||
.event-title {
|
||||
grid-area: titl;
|
||||
font-size: 3vw;
|
||||
font-weight: 600;
|
||||
text-decoration: underline 0.5vh;
|
||||
text-decoration-color: var(--accent-color-override, $accent-color);
|
||||
padding-top: 0.2vh;
|
||||
padding-left: 1vw;
|
||||
}
|
||||
|
||||
.today-container {
|
||||
grid-area: schd;
|
||||
padding: 2.5vh 2vw;
|
||||
overflow: hidden;
|
||||
margin-top: 3vh;
|
||||
height: 95%;
|
||||
|
||||
.today-header-block {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5vh;
|
||||
|
||||
.nav {
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-bottom: 2.5vh;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
background-color: $today-item-bg;
|
||||
width: 0.7vw;
|
||||
height: 0.7vw;
|
||||
border-radius: 0.35vw;
|
||||
margin-left: 0.5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.public-container {
|
||||
grid-area: publ;
|
||||
|
||||
&--hidden {
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
}
|
||||
|
||||
.clock-container {
|
||||
grid-area: time;
|
||||
border-radius: 1vw;
|
||||
}
|
||||
|
||||
.info {
|
||||
grid-area: info;
|
||||
display: grid;
|
||||
padding: 2.5vh 2vw;
|
||||
|
||||
grid-template-rows: 3vh minmax(0, 1fr);
|
||||
grid-template-columns: 3fr 1fr;
|
||||
grid-template-areas:
|
||||
'titl .'
|
||||
'binf qr';
|
||||
gap: 0.5vw;
|
||||
|
||||
&__message {
|
||||
grid-area: binf;
|
||||
font-size: 1.5vw;
|
||||
line-height: 2vw;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qr {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
grid-area: qr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import useFitText from 'use-fit-text';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatDisplay } from '../../../common/utils/dateConfig';
|
||||
import {
|
||||
formatEventList,
|
||||
@@ -11,7 +13,7 @@ import {
|
||||
} from '../../../common/utils/eventsManager';
|
||||
import { formatTime, stringFromMillis } from '../../../common/utils/time';
|
||||
|
||||
import style from './StudioClock.module.scss';
|
||||
import './StudioClock.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: false,
|
||||
@@ -19,15 +21,18 @@ const formatOptions = {
|
||||
};
|
||||
|
||||
export default function StudioClock(props) {
|
||||
const { title, time, backstageEvents, selectedId, nextId, onAir } = props;
|
||||
const { title, time, backstageEvents, selectedId, nextId, onAir, viewSettings } = props;
|
||||
|
||||
// deferring rendering seems to affect styling (font and useFitText)
|
||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
||||
|
||||
const [schedule, setSchedule] = useState([]);
|
||||
|
||||
const activeIndicators = [...Array(12).keys()];
|
||||
const secondsIndicators = [...Array(60).keys()];
|
||||
const MAX_TITLES = 10;
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Studio Clock';
|
||||
}, []);
|
||||
@@ -36,7 +41,6 @@ export default function StudioClock(props) {
|
||||
useEffect(() => {
|
||||
if (backstageEvents == null) return;
|
||||
|
||||
|
||||
const delayed = getEventsWithDelay(backstageEvents);
|
||||
const events = delayed.filter((e) => e.type === 'event');
|
||||
const trimmed = trimEventlist(events, selectedId, MAX_TITLES);
|
||||
@@ -47,29 +51,28 @@ export default function StudioClock(props) {
|
||||
}, [backstageEvents, nextId, selectedId] );
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
const [, , secondsNow] = stringFromMillis(time.clock).split(':');
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<div className='studio-clock'>
|
||||
<NavLogo />
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.time}>{clock}</div>
|
||||
<div className='clock-container'>
|
||||
<div className='studio-timer'>{clock}</div>
|
||||
<div
|
||||
ref={titleRef}
|
||||
className={style.nextTitle}
|
||||
className='next-title'
|
||||
style={{ fontSize: titleFontSize, height: '10vh', width: '100%', maxWidth: '82%' }}
|
||||
>
|
||||
{title.titleNext}
|
||||
</div>
|
||||
<div className={time.isNegative ? style.nextCountdown : style.nextCountdown__overtime}>
|
||||
<div className={time.isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}>
|
||||
{selectedId != null && formatDisplay(time.running)}
|
||||
</div>
|
||||
<div className={style.indicators}>
|
||||
<div className='clock-indicators'>
|
||||
{activeIndicators.map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={style.hours__active}
|
||||
className='hours hours--active'
|
||||
style={{
|
||||
transform: `rotate(${(360 / 12) * i - 90}deg) translateX(40vh)`,
|
||||
}}
|
||||
@@ -78,7 +81,7 @@ export default function StudioClock(props) {
|
||||
{secondsIndicators.map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i <= secondsNow ? style.min__active : style.min}
|
||||
className={i <= secondsNow ? 'min min--active' : 'min'}
|
||||
style={{
|
||||
transform: `rotate(${(360 / 60) * i - 90}deg) translateX(43vh)`,
|
||||
}}
|
||||
@@ -86,14 +89,14 @@ export default function StudioClock(props) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.scheduleContainer}>
|
||||
<div className={onAir ? style.onAir : style.onAir__idle}>ON AIR</div>
|
||||
<div className={style.schedule}>
|
||||
<div className='schedule-container'>
|
||||
<div className={onAir ? 'onAir' : 'onAir onAir--idle'}>ON AIR</div>
|
||||
<div className='schedule'>
|
||||
<ul>
|
||||
{schedule.map((s) => (
|
||||
<li
|
||||
key={s.id}
|
||||
className={s.isNow ? style.now : s.isNext ? style.next : ''}
|
||||
className={s.isNow ? 'now' : s.isNext ? 'next' : ''}
|
||||
style={{ borderLeft: `4px solid ${s.colour !== '' ? s.colour : 'transparent'}` }}
|
||||
>
|
||||
{`${s.time} ${s.title}`}
|
||||
@@ -113,4 +116,5 @@ StudioClock.propTypes = {
|
||||
selectedId: PropTypes.string,
|
||||
nextId: PropTypes.string,
|
||||
onAir: PropTypes.bool,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
|
||||
+36
-54
@@ -1,3 +1,5 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
@font-face {
|
||||
font-family: "digital-clock";
|
||||
src: local('digital-7'), url('./../../../assets/fonts/digital-7.monoitalic.ttf') format('truetype') ;
|
||||
@@ -15,7 +17,7 @@ $red-idle: #300000;
|
||||
$cyan-active: #0ff;
|
||||
$cyan-idle: #0aa;
|
||||
|
||||
.container {
|
||||
.studio-clock {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
@@ -23,14 +25,14 @@ $cyan-idle: #0aa;
|
||||
height: 100vh;
|
||||
padding: 1vw;
|
||||
|
||||
background: #000;
|
||||
background: var(--background-color-override, #000);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 95vh 1fr;
|
||||
gap: 2vw;
|
||||
grid-template-areas: "clck schd";
|
||||
|
||||
.clockContainer {
|
||||
.clock-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -44,114 +46,92 @@ $cyan-idle: #0aa;
|
||||
font-family: digital-clock, monospace;
|
||||
text-transform: uppercase;
|
||||
|
||||
.indicators {
|
||||
.clock-indicators {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.min,
|
||||
.min__active,
|
||||
.hours,
|
||||
.hours__active {
|
||||
.hours {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
background: $red-idle;
|
||||
|
||||
&--active {
|
||||
background: $red-active;
|
||||
box-shadow: 0 0 10px 2px rgba(255, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.min,
|
||||
.min__active {
|
||||
.min {
|
||||
min-height: $size-min;
|
||||
width: $size-min;
|
||||
top: calc(50% - #{$half_min});
|
||||
left: calc(50% - #{$half_min});
|
||||
}
|
||||
|
||||
.hours,
|
||||
.hours__active {
|
||||
.hours {
|
||||
min-height: $size-hours;
|
||||
width: $size-hours;
|
||||
top: calc(50% - #{$half_hours});
|
||||
left: calc(50% - #{$half_hours});
|
||||
}
|
||||
|
||||
.min__active,
|
||||
.hours__active {
|
||||
background: $red-active;
|
||||
box-shadow: 0 0 10px 2px rgba(255, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
.studio-timer {
|
||||
color: $red-active;
|
||||
font-size: calc(#{$clock-size} / 3);
|
||||
margin-top: calc(50% - calc(#{$clock-size} / 7));
|
||||
line-height: 0.8em;
|
||||
}
|
||||
|
||||
.timeAA {
|
||||
color: $red-active;
|
||||
font-size: calc(#{$clock-size} / 4.5);
|
||||
margin-top: calc(50% - calc(#{$clock-size} / 7));
|
||||
line-height: 0.8em;
|
||||
}
|
||||
|
||||
.nextTitle:after,
|
||||
.nextCountdown:after,
|
||||
.nextCountdown__overtime:after {
|
||||
.next-title:after,
|
||||
.next-title:after,
|
||||
.next-countdown__overtime:after {
|
||||
content: '\200b';
|
||||
}
|
||||
|
||||
.nextTitle {
|
||||
.next-title {
|
||||
color: $cyan-idle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nextCountdown,
|
||||
.nextCountdown__overtime {
|
||||
.next-countdown {
|
||||
font-size: 10vh;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.nextCountdown {
|
||||
color: $cyan-active;
|
||||
text-shadow: rgb(0, 100, 100) 0 0 20px;
|
||||
|
||||
&--overtime {
|
||||
color: darken($red-active, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.nextCountdown::before {
|
||||
.next-countdown::before {
|
||||
content: '-';
|
||||
}
|
||||
|
||||
.nextCountdown__overtime {
|
||||
color: darken($red-active, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============= SCHEDULE STUFF =================*/
|
||||
|
||||
.scheduleContainer {
|
||||
.schedule-container {
|
||||
grid-area: schd;
|
||||
margin: 4vh 0;
|
||||
font-family: digital-clock, monospace;
|
||||
text-transform: uppercase;
|
||||
|
||||
.onAir,
|
||||
.onAir__idle {
|
||||
.onAir {
|
||||
padding-bottom: 2vh;
|
||||
font-size: 15vh;
|
||||
line-height: 0.9em;
|
||||
}
|
||||
|
||||
.onAir {
|
||||
color: $red-active;
|
||||
}
|
||||
|
||||
.onAir__idle {
|
||||
color: $red-idle;
|
||||
&--idle {
|
||||
color: $red-idle;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule {
|
||||
|
||||
ul {
|
||||
color: $cyan-idle;
|
||||
font-size: 3.75vh;
|
||||
@@ -161,6 +141,7 @@ $cyan-idle: #0aa;
|
||||
|
||||
li {
|
||||
margin-bottom: 1.5vh;
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
|
||||
.now {
|
||||
@@ -175,13 +156,14 @@ $cyan-idle: #0aa;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.container {
|
||||
.studio-clock {
|
||||
display: grid;
|
||||
grid-template-areas: "clck";
|
||||
grid-template-columns: 100%;
|
||||
place-content: center;
|
||||
}
|
||||
.scheduleContainer {
|
||||
display: none;
|
||||
|
||||
.schedule-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
.container,
|
||||
.containerFinished {
|
||||
background: $bg-black;
|
||||
height: 100vh;
|
||||
color: $title-white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1vw;
|
||||
border: 1vw solid transparent;
|
||||
}
|
||||
|
||||
.containerFinished {
|
||||
border: 1vw solid $ontime-pink-variant;
|
||||
color: $ontime-pink-variant;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.timer,
|
||||
.timerPaused {
|
||||
font-family: "Arial Black", sans-serif;
|
||||
font-size: 20vw;
|
||||
position: relative;
|
||||
color: inherit;
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.timerPaused {
|
||||
opacity: 0.6;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
/* =================== OVERLAY ===================*/
|
||||
|
||||
.messageOverlay,
|
||||
.messageOverlayActive {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: $bg-overlay;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.messageOverlayActive {
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: inherit;
|
||||
padding: 2vw;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
color: white;
|
||||
transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
font-size: 15vw;
|
||||
line-height: 30vh;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -3,13 +3,15 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import TimerDisplay from 'common/components/countdown/TimerDisplay';
|
||||
import MyProgressBar from 'common/components/myProgressBar/MyProgressBar';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import TitleCard from 'common/components/views/TitleCard';
|
||||
import TitleCard from 'common/components/title-card/TitleCard';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
import style from './Timer.module.scss';
|
||||
import './Timer.scss';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
@@ -17,30 +19,32 @@ const formatOptions = {
|
||||
};
|
||||
|
||||
export default function Timer(props) {
|
||||
const { general, pres, title, time } = props;
|
||||
const { general, pres, title, time, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [elapsed, setElapsed] = useState(true);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Timer';
|
||||
}, []);
|
||||
|
||||
// defer rendering until we load stylesheets
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// eg. http://localhost:3000/timer?progress=up
|
||||
// Check for user options
|
||||
useEffect(() => {
|
||||
// progress: selector
|
||||
// Should be 'up' or 'down'
|
||||
const progress = searchParams.get('progress');
|
||||
if (progress === 'up') {
|
||||
setElapsed(true);
|
||||
} else if (progress === 'down') {
|
||||
setElapsed(false);
|
||||
}
|
||||
}, [searchParams]);
|
||||
// progress: selector
|
||||
// Should be 'up' or 'down'
|
||||
const progress = searchParams.get('progress');
|
||||
if (progress === 'up') {
|
||||
setElapsed(true);
|
||||
} else if (progress === 'down') {
|
||||
setElapsed(false);
|
||||
}
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
const showOverlay = pres.text !== '' && pres.visible;
|
||||
const isPlaying = time.playstate !== 'pause';
|
||||
const normalisedTime = Math.max(time.running, 0);
|
||||
@@ -62,21 +66,21 @@ export default function Timer(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={time.finished ? style.container__grayFinished : style.container__gray}>
|
||||
<div className={showOverlay ? style.messageOverlayActive : style.messageOverlay}>
|
||||
<div className={style.message}>{pres.text}</div>
|
||||
<div className={time.finished ? 'stage-timer stage-timer--finished' : 'stage-timer'}>
|
||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||
<div className='message'>{pres.text}</div>
|
||||
</div>
|
||||
|
||||
<NavLogo />
|
||||
|
||||
<div className={style.clockContainer}>
|
||||
<div className={style.label}>Time Now</div>
|
||||
<div className={style.clock}>{clock}</div>
|
||||
<div className='clock-container'>
|
||||
<div className='label'>Time Now</div>
|
||||
<div className='clock'>{clock}</div>
|
||||
</div>
|
||||
|
||||
<div className={style.timerContainer}>
|
||||
<div className='timer-container'>
|
||||
{time.finished ? (
|
||||
<div className={style.finished}>
|
||||
<div className='end-message'>
|
||||
{general.endMessage == null || general.endMessage === '' ? (
|
||||
<TimerDisplay time={time.running} isNegative={time.isNegative} hideZeroHours />
|
||||
) : (
|
||||
@@ -84,14 +88,18 @@ export default function Timer(props) {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={isPlaying ? style.countdown : style.countdownPaused}>
|
||||
<div className={isPlaying ? 'timer' : 'timer--paused'}>
|
||||
<TimerDisplay time={normalisedTime} hideZeroHours />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!time.finished && (
|
||||
<div className={isPlaying ? style.progressContainer : style.progressContainerPaused}>
|
||||
<div
|
||||
className={
|
||||
isPlaying ? 'progress-container' : 'progress-container progress-container--paused'
|
||||
}
|
||||
>
|
||||
<MyProgressBar
|
||||
now={normalisedTime}
|
||||
complete={time.durationSeconds}
|
||||
@@ -103,7 +111,7 @@ export default function Timer(props) {
|
||||
<AnimatePresence>
|
||||
{title.showNow && (
|
||||
<motion.div
|
||||
className={style.nowContainer}
|
||||
className='event now'
|
||||
key='now'
|
||||
variants={titleVariants}
|
||||
initial='hidden'
|
||||
@@ -123,7 +131,7 @@ export default function Timer(props) {
|
||||
<AnimatePresence>
|
||||
{title.showNext && (
|
||||
<motion.div
|
||||
className={style.nextContainer}
|
||||
className='event next'
|
||||
key='next'
|
||||
variants={titleVariants}
|
||||
initial='hidden'
|
||||
@@ -148,4 +156,5 @@ Timer.propTypes = {
|
||||
pres: PropTypes.object,
|
||||
title: PropTypes.object,
|
||||
time: PropTypes.object,
|
||||
viewSettings: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
.container__gray,
|
||||
.container__grayFinished {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
|
||||
background: $bg-black;
|
||||
height: 100vh;
|
||||
color: $title-white;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 5vw 1fr 1fr;
|
||||
grid-template-rows: auto 1fr auto minmax(25vh, auto);
|
||||
grid-template-areas:
|
||||
' clck clck .... .... ....'
|
||||
' timr timr timr timr timr'
|
||||
' prog prog prog prog prog'
|
||||
' now now .... next next';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
/* =================== TITLES ===================*/
|
||||
|
||||
.nowContainer,
|
||||
.nextContainer {
|
||||
background-color: $bg-gray-1000;
|
||||
padding: 1vh 2vw;
|
||||
border-radius: 1vw;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.nowContainer {
|
||||
grid-area: now;
|
||||
}
|
||||
|
||||
.nextContainer {
|
||||
grid-area: next;
|
||||
}
|
||||
|
||||
/* =================== MAIN ===================*/
|
||||
|
||||
.timerContainer,
|
||||
.timerContainerFinished {
|
||||
grid-area: timr;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.finished {
|
||||
text-align: center;
|
||||
font-size: 12vw;
|
||||
line-height: 18vw;
|
||||
font-weight: 600;
|
||||
color: $ontime-pink-variant;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.progressContainer,
|
||||
.progressContainerPaused {
|
||||
grid-area: prog;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.countdownPaused,
|
||||
.progressContainerPaused {
|
||||
opacity: 0.6;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.container__grayFinished {
|
||||
border: 1vw solid $ontime-pink-variant;
|
||||
}
|
||||
|
||||
/* =================== OVERLAY ===================*/
|
||||
|
||||
.messageOverlay,
|
||||
.messageOverlayActive {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: $bg-overlay;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.messageOverlayActive {
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: inherit;
|
||||
padding: 2vw;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
color: white;
|
||||
transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
font-size: 15vw;
|
||||
line-height: 30vh;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* =================== MAIN ===================*/
|
||||
|
||||
.clockContainer {
|
||||
grid-area: clck;
|
||||
padding: 1vh 2vw;
|
||||
}
|
||||
|
||||
.clock {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 4vw;
|
||||
letter-spacing: 0.4vw;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.stage-timer {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
|
||||
background: var(--background-color-override, $viewer-background-color);
|
||||
height: 100vh;
|
||||
color: var(--color-override, $viewer-color);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 5vw 1fr 1fr;
|
||||
grid-template-rows: auto 1fr auto minmax(25vh, auto);
|
||||
grid-template-areas:
|
||||
' clck clck .... .... ....'
|
||||
' timr timr timr timr timr'
|
||||
' prog prog prog prog prog'
|
||||
' now now .... next next';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
|
||||
&--finished {
|
||||
border: 1vw solid $timer-finished-color;
|
||||
}
|
||||
|
||||
|
||||
/* =================== TITLES ===================*/
|
||||
|
||||
.event {
|
||||
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
|
||||
padding: 1vh 2vw;
|
||||
border-radius: 1vw;
|
||||
max-width: 100%;
|
||||
|
||||
&.now {
|
||||
grid-area: now;
|
||||
|
||||
}
|
||||
&.next {
|
||||
grid-area: next;
|
||||
}
|
||||
}
|
||||
|
||||
/* =================== MAIN ===================*/
|
||||
|
||||
.timer-container {
|
||||
grid-area: timr;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
color: white;
|
||||
|
||||
.end-message {
|
||||
text-align: center;
|
||||
font-size: 12vw;
|
||||
line-height: 18vw;
|
||||
font-weight: 600;
|
||||
color: $timer-finished-color;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.timer {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
grid-area: prog;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.timer--paused,
|
||||
.progress-container--paused {
|
||||
opacity: 0.6;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
|
||||
/* =================== OVERLAY ===================*/
|
||||
|
||||
.message-overlay {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: $viewer-overlay-bg-color;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
|
||||
&--active {
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
width: inherit;
|
||||
padding: 2vw;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
color: white;
|
||||
transform: translate(-50%, -50%);
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
font-size: 15vw;
|
||||
line-height: 30vh;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* =================== MAIN ===================*/
|
||||
|
||||
.clock-container {
|
||||
grid-area: clck;
|
||||
padding: 1vh 2vw;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
}
|
||||
|
||||
.clock {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 4vw;
|
||||
letter-spacing: 0.4vw;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user