mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 03:13:47 +00:00
Feat/132 (#198)
This commit is contained in:
@@ -32,6 +32,7 @@ server/src/preloaded-db/db.json
|
||||
server/src/models/db.json
|
||||
TODO.md
|
||||
ontime-db/
|
||||
ontime-external/
|
||||
|
||||
# vscode stuff
|
||||
.vscode/*
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "1.7.3",
|
||||
"version": "1.8.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.3.2",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#121212" />
|
||||
<meta name="onsite" content="onsite - event timer manager" />
|
||||
<meta name="ontime" content="ontime - event timer manager" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link
|
||||
rel="icon"
|
||||
|
||||
@@ -11,19 +11,19 @@ const Editor = lazy(() => import('features/editors/ProtectedEditor'));
|
||||
const Table = lazy(() => import('features/table/ProtectedTable'));
|
||||
|
||||
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
|
||||
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer'));
|
||||
const MinimalTimerView = lazy(() => import('features/viewers/minimal-timer/MinimalTimer'));
|
||||
const Countdown = lazy(() => import('features/viewers/countdown/Countdown'));
|
||||
|
||||
const StageManager = lazy(() => import('features/viewers/backstage/StageManager'));
|
||||
const Public = lazy(() => import('features/viewers/foh/Public'));
|
||||
const Lower = lazy(() => import('features/viewers/production/lower/LowerWrapper'));
|
||||
const Pip = lazy(() => import('features/viewers/production/Pip'));
|
||||
const Backstage = lazy(() => import('features/viewers/backstage/Backstage'));
|
||||
const Public = lazy(() => import('features/viewers/public/Public'));
|
||||
const Lower = lazy(() => import('features/viewers/lower-thirds/LowerWrapper'));
|
||||
const Pip = lazy(() => import('features/viewers/picture-in-picture/Pip'));
|
||||
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
|
||||
|
||||
const STimer = withSocket(TimerView);
|
||||
const SMinimalTimer = withSocket(MinimalTimerView);
|
||||
const SCountdown = withSocket(Countdown);
|
||||
const SStageManager = withSocket(StageManager);
|
||||
const SBackstage = withSocket(Backstage);
|
||||
const SPublic = withSocket(Public);
|
||||
const SLowerThird = withSocket(Lower);
|
||||
const SPip = withSocket(Pip);
|
||||
@@ -66,8 +66,8 @@ export default function AppRouter() {
|
||||
|
||||
<Route path='/countdown' element={<SCountdown />} />
|
||||
|
||||
<Route path='/sm' element={<SStageManager />} />
|
||||
<Route path='/backstage' element={<SStageManager />} />
|
||||
<Route path='/sm' element={<SBackstage />} />
|
||||
<Route path='/backstage' element={<SBackstage />} />
|
||||
|
||||
<Route path='/public' element={<SPublic />} />
|
||||
<Route path='/pip' element={<SPip />} />
|
||||
|
||||
@@ -6,6 +6,7 @@ export const EVENTS_TABLE = ['events'];
|
||||
export const APP_TABLE = ['appinfo'];
|
||||
export const OSC_SETTINGS = ['oscSettings'];
|
||||
export const APP_SETTINGS = ['appSettings'];
|
||||
export const VIEW_SETTINGS = ['viewSettings'];
|
||||
|
||||
export const FEAT_EVENTLIST = ['feat-eventList'];
|
||||
export const FEAT_MESSAGECONTROL = ['feat-messagecontrol'];
|
||||
@@ -18,7 +19,7 @@ export const TIMER = ['ontime-timer'];
|
||||
* @description finds server path given the current location, it
|
||||
* @return {*}
|
||||
*/
|
||||
const calculateServer = () => {
|
||||
export const calculateServer = () => {
|
||||
if (process.env?.NODE_ENV === 'development') {
|
||||
return `http://localhost:${STATIC_PORT}`;
|
||||
}
|
||||
@@ -30,3 +31,6 @@ export const eventURL = `${serverURL}/${EVENT_TABLE}`;
|
||||
export const eventsURL = `${serverURL}/${EVENTS_TABLE}`;
|
||||
export const playbackURL = `${serverURL}/playback`;
|
||||
export const ontimeURL = `${serverURL}/ontime`;
|
||||
|
||||
export const stylesPath = 'external/styles/override.css';
|
||||
export const overrideStylesURL = `serverURL/${stylesPath}`;
|
||||
|
||||
@@ -67,6 +67,14 @@ export const oscPlaceholderSettings = {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for view settings
|
||||
* @type {{overrideCSS: boolean}}
|
||||
*/
|
||||
export const viewsPlaceholder = {
|
||||
overrideStyles: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for httpSettings
|
||||
* @type {{onStart: {url: string, enabled: boolean}, onLoad: {url: string, enabled: boolean}, onPause: {url: string, enabled: boolean}, onFinish: {url: string, enabled: boolean}, onUpdate: {url: string, enabled: boolean}, onStop: {url: string, enabled: boolean}}}
|
||||
@@ -163,6 +171,21 @@ export const getInfo = async () => {
|
||||
*/
|
||||
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve view settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getView = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/views`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate view settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postView = async (data) => axios.post(`${ontimeURL}/views`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve aliases
|
||||
* @return {Promise}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { memo } from 'react';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import styles from './TimerDisplay.module.scss';
|
||||
import './TimerDisplay.scss';
|
||||
|
||||
const TimerDisplay = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
|
||||
@@ -11,14 +11,9 @@ const TimerDisplay = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
? '-- : -- : --'
|
||||
: formatDisplay(time, hideZeroHours);
|
||||
|
||||
const classes = `${small ? styles.countdownClockSmall : styles.countdownClock}
|
||||
${isNegative ? styles.negative : ''}`;
|
||||
const classes = `timer ${small ? 'timer--small' : ''} ${isNegative ? 'timer--finished' : ''}`;
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{display}
|
||||
</div>
|
||||
);
|
||||
return <div className={classes}>{display}</div>;
|
||||
};
|
||||
|
||||
export default memo(TimerDisplay);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
/* Common */
|
||||
.countdownClock,
|
||||
.countdownClockSmall {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
line-height: 0.9;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
/* Viewers */
|
||||
.countdownClock {
|
||||
font-size: 21vw;
|
||||
line-height: 21vw;
|
||||
text-align: center;
|
||||
letter-spacing: 1vw;
|
||||
}
|
||||
|
||||
/* Editor */
|
||||
.countdownClockSmall {
|
||||
font-size: 4em;
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.timer {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: var(--timer-color-override, $viewer-color);
|
||||
font-size: 21vw;
|
||||
line-height: 21vw;
|
||||
text-align: center;
|
||||
letter-spacing: 1vw;
|
||||
|
||||
&--small {
|
||||
font-size: 4em;
|
||||
line-height: 0.9;
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--finished {
|
||||
color: $timer-finished-color;
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@ class ErrorBoundary extends React.Component {
|
||||
});
|
||||
try {
|
||||
this.context.emitError(error.toString());
|
||||
} catch {
|
||||
console.log('Unable to emit error')
|
||||
} catch (e) {
|
||||
console.log('Unable to emit error', error, e )
|
||||
}
|
||||
this.reportContent = `${error} ${info.componentStack}`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const navigatorConstants = [
|
||||
{ url: '/timer', label: 'Timer' },
|
||||
{ url: '/minimal', label: 'Minimal Timer' },
|
||||
{ url: '/sm', label: 'Backstage' },
|
||||
{ url: '/backstage', label: 'Backstage' },
|
||||
{ url: '/public', label: 'Public' },
|
||||
{ url: '/lower', label: 'Lower Thirds' },
|
||||
{ url: '/pip', label: 'PiP' },
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import Empty from '../state/Empty';
|
||||
|
||||
import TodayItem from './TodayItem';
|
||||
|
||||
import style from './Paginator.module.scss';
|
||||
import './Paginator.scss';
|
||||
|
||||
export default function Paginator(props) {
|
||||
const {
|
||||
@@ -70,7 +70,7 @@ export default function Paginator(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.entries}>
|
||||
<div className='paginator entries'>
|
||||
{page.map((e) => {
|
||||
if (e.id === selectedId) selectedState = 1;
|
||||
else if (selectedState === 1) selectedState = 2;
|
||||
@@ -0,0 +1,62 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.paginator.entries {
|
||||
width: 100%;
|
||||
|
||||
.entry-times {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
|
||||
&--now,
|
||||
&--future {
|
||||
color: var(--color-override, $viewer-color);
|
||||
}
|
||||
|
||||
&--past {
|
||||
font-size: 1.2vw;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&--now {
|
||||
line-height: 5vh;
|
||||
background-color: $now-bg-color;
|
||||
border-bottom: 0.5vh solid;
|
||||
border-bottom-color: var(--accent-color-override, $accent-color);
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
&--future {
|
||||
background-color: $future-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.backstage-indicator {
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
|
||||
&::before {
|
||||
content: '*';
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { formatTime } from '../../utils/time';
|
||||
|
||||
import style from './Paginator.module.scss';
|
||||
import './Paginator.scss';
|
||||
|
||||
export default function TodayItem(props) {
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
|
||||
@@ -15,16 +15,16 @@ export default function TodayItem(props) {
|
||||
const userColour = colour !== '' ? colour : 'transparent';
|
||||
|
||||
// select styling
|
||||
let selectStyle = style.entryPast;
|
||||
if (selected === 1) selectStyle = style.entryNow;
|
||||
else if (selected === 2) selectStyle = style.entryFuture;
|
||||
let selectStyle = 'entry--past';
|
||||
if (selected === 1) selectStyle = 'entry--now';
|
||||
else if (selected === 2) selectStyle = 'entry--future';
|
||||
return (
|
||||
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : ''}`}>
|
||||
<div className={`entry ${selectStyle}`} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className='entry-times'>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className={style.entryTitle}>{title}</div>
|
||||
{backstageEvent && <div className={style.backstageInd} />}
|
||||
<div className='entry-title'>{title}</div>
|
||||
{backstageEvent && <div className='backstage-indicator' />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './TitleSide.scss';
|
||||
|
||||
export default function TitleSide(props) {
|
||||
const { type, label, title, subtitle, presenter } = props;
|
||||
|
||||
const isNext = type === 'next';
|
||||
|
||||
return (
|
||||
<div className='title-side'>
|
||||
<div className='label'>{label}</div>
|
||||
<div className={isNext ? 'title next' : 'title now'}>{title}</div>
|
||||
<div className={isNext ? 'presenter next' : 'presenter now'}>{presenter}</div>
|
||||
<div className={isNext ? 'subtitle next' : 'subtitle now'}>{subtitle}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TitleSide.propTypes = {
|
||||
type: PropTypes.oneOf(['now', 'next']),
|
||||
label: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
presenter: PropTypes.string,
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.title-side {
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle,
|
||||
.presenter {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $title-color;
|
||||
font-weight: 600;
|
||||
|
||||
&.now {
|
||||
font-size: 2.5vw;
|
||||
}
|
||||
|
||||
&.next {
|
||||
font-size: 2vw;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: $subtitle-color;
|
||||
&.now {
|
||||
font-size: 1.2vw;
|
||||
margin-top: -1vh;
|
||||
}
|
||||
|
||||
&.next {
|
||||
font-size: 1.1vw;
|
||||
margin-top: -0.8vh;
|
||||
}
|
||||
}
|
||||
|
||||
.presenter {
|
||||
font-weight: 400;
|
||||
color: $subtitle-color;
|
||||
|
||||
&.now {
|
||||
font-size: 2vw;
|
||||
}
|
||||
|
||||
&.next {
|
||||
font-size: 1.5vw;
|
||||
}
|
||||
}
|
||||
|
||||
.title:after,
|
||||
.subtitle:after,
|
||||
.presenter:after {
|
||||
content: '\200b';
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
.entries {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entryTimes {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entryPast {
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
.entryPast > .entryTitle {
|
||||
color: $text-gray-disabled;
|
||||
}
|
||||
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.entryFuture {
|
||||
background-color: $bg-gray-950;
|
||||
}
|
||||
|
||||
.entryNow {
|
||||
line-height: 5vh;
|
||||
background-color: $bg-gray-900;
|
||||
border-bottom: 0.5vh solid $ontime-pink;
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.backstageInd {
|
||||
color: $ontime-pink;
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
}
|
||||
|
||||
.backstageInd::before {
|
||||
content: '*';
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from './TitleSide.module.scss';
|
||||
|
||||
export default function TitleSide(props) {
|
||||
const { type, label, title, subtitle, presenter } = props;
|
||||
|
||||
const titleStyle = type === 'next' ? style.nextTitle : style.nowTitle;
|
||||
const subStyle = type === 'next' ? style.nextSubtitle : style.nowSubtitle;
|
||||
const presStyle = type === 'next' ? style.nextPresenter : style.nowPresenter;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.label}>{label}</div>
|
||||
<div className={titleStyle}>{title}</div>
|
||||
<div className={presStyle}>{presenter}</div>
|
||||
<div className={subStyle}>{subtitle}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
TitleSide.propTypes = {
|
||||
type: PropTypes.oneOf(['now', 'next']),
|
||||
label: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
presenter: PropTypes.string,
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
.label {
|
||||
@include card-label;
|
||||
}
|
||||
|
||||
.nowTitle,
|
||||
.nextTitle {
|
||||
@include card-title;
|
||||
}
|
||||
|
||||
.nowSubtitle,
|
||||
.nowPresenter,
|
||||
.nextSubtitle,
|
||||
.nextPresenter {
|
||||
color: $subtitle-gray;
|
||||
}
|
||||
|
||||
.nowTitle {
|
||||
font-size: 2.5vw;
|
||||
}
|
||||
|
||||
.nowPresenter {
|
||||
font-size: 2vw;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.nowSubtitle {
|
||||
font-size: 1.2vw;
|
||||
margin-top: -1vh;
|
||||
}
|
||||
|
||||
.nextTitle {
|
||||
font-size: 2vw;
|
||||
}
|
||||
|
||||
.nextSubtitle {
|
||||
margin-top: -0.8vh;
|
||||
font-size: 1.1vw;
|
||||
}
|
||||
|
||||
.nextPresenter {
|
||||
font-weight: 400;
|
||||
font-size: 1.5vw;
|
||||
}
|
||||
|
||||
.nowTitle:after,
|
||||
.nowSubtitle:after,
|
||||
.nowPresenter:after,
|
||||
.nextTitle:after,
|
||||
.nextSubtitle:after,
|
||||
.nextPresenter:after {
|
||||
content: '\200b';
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export const useSocket = () => {
|
||||
};
|
||||
|
||||
function SocketProvider({ children }: SocketProviderProps) {
|
||||
const [socket, setSocket] = useState({} as Socket);
|
||||
const [socket, setSocket] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const socketInstance = io(serverURL, { transports: ["websocket"] });
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const scriptTagId = 'ontime-override';
|
||||
export const useRuntimeStylesheet = (pathToFile) => {
|
||||
const [shouldRender, setShouldRender] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
let response = await fetch(pathToFile);
|
||||
if (response.ok) {
|
||||
return await response.text();
|
||||
}
|
||||
};
|
||||
|
||||
if (!pathToFile) {
|
||||
document.getElementById(scriptTagId)?.remove();
|
||||
setShouldRender(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.getElementById(scriptTagId)) {
|
||||
setShouldRender(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setShouldRender(false);
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.rel = 'stylesheet';
|
||||
styleSheet.setAttribute('id', scriptTagId);
|
||||
|
||||
fetchData()
|
||||
.then((data) => {
|
||||
styleSheet.innerHTML = data;
|
||||
document.head.append(styleSheet);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Error loading stylesheet: ${error}`);
|
||||
})
|
||||
.finally(() => {
|
||||
// schedule render for next tick
|
||||
setTimeout(() => setShouldRender(true), 0);
|
||||
});
|
||||
}, [pathToFile]);
|
||||
|
||||
return { shouldRender };
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -64,6 +64,7 @@ $subtitle-gray: #aaa;
|
||||
|
||||
//////////////////////////////////// block elements
|
||||
$block-delay-color: #ecc94b;
|
||||
$delay-text: #d69e2e;
|
||||
$block-delay-border: #d69e2e55;
|
||||
$block-block-color: #805ad5;
|
||||
$block-icon-drag: $bg-gray-100;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@use 'main' as *;
|
||||
|
||||
// General styling
|
||||
$accent-color: $ontime-pink; // --accent-color-override
|
||||
$delay-color: $delay-text;
|
||||
|
||||
// Main Properties of a viewer
|
||||
$viewer-background-color: $bg-black; // --background-color-override
|
||||
$viewer-color: $title-white; // --color-override
|
||||
$viewer-outdent-bg-color: $bg-gray-1100; // --outdent-background-color-override
|
||||
$viewer-overlay-bg-color: $bg-overlay;
|
||||
|
||||
// Properties related to timer
|
||||
$timer-finished-color: $ontime-pink-variant;
|
||||
|
||||
// View specific - Pip
|
||||
$pip-bg-color: $bg-black-100;
|
||||
$pip-border-color: $bg-black-200;
|
||||
$pip-text-color: $bg-gray-900;
|
||||
$today-item-bg: $bg-gray-700;
|
||||
|
||||
// View specific - Titles
|
||||
$title-color: #ddd;
|
||||
$subtitle-color: #aaa;
|
||||
|
||||
// View specific - Paginator
|
||||
$now-bg-color: #ffffff11;
|
||||
$future-bg-color: #ffffff05;
|
||||
@@ -32,58 +32,57 @@
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
.infoContainer > div {
|
||||
.info-container > div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nextContainer > div,
|
||||
.nowContainer > div {
|
||||
.next-container > div,
|
||||
.now-container > div {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.nowContainer,
|
||||
.nextContainer,
|
||||
.todayContainer {
|
||||
.now-container,
|
||||
.next-container,
|
||||
.today-container {
|
||||
background-color: $bg-gray-1000;
|
||||
padding: 1vh 2vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clockContainer,
|
||||
.countdownContainer,
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
background-color: $bg-gray-1000;
|
||||
.clock-container,
|
||||
.countdown-container,
|
||||
.public-container,
|
||||
.public-container--hidden {
|
||||
padding: 1vh 1vw;
|
||||
}
|
||||
|
||||
.publicContainer {
|
||||
.public-container {
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.publicContainerHidden {
|
||||
.public-container--hidden {
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.todayContainer,
|
||||
.infoContainer {
|
||||
.today-container,
|
||||
.info-container {
|
||||
background-color: $bg-gray-1100;
|
||||
padding: 2.5vh 2vw;
|
||||
}
|
||||
|
||||
.infoContainer,
|
||||
.todayContainer,
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
.info-container,
|
||||
.today-container,
|
||||
.public-container,
|
||||
.public-container--hidden {
|
||||
border-radius: 1vw;
|
||||
}
|
||||
|
||||
.nowContainer,
|
||||
.nextContainer {
|
||||
.now-container,
|
||||
.next-container {
|
||||
margin-left: -1vw;
|
||||
}
|
||||
|
||||
@@ -102,7 +101,7 @@
|
||||
color: $clocks;
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
.info-container {
|
||||
grid-area: info;
|
||||
display: grid;
|
||||
|
||||
@@ -114,14 +113,14 @@
|
||||
gap: 0.5vw;
|
||||
}
|
||||
|
||||
.infoMessages {
|
||||
.info-messages {
|
||||
grid-area: binf;
|
||||
font-size: 1.5vw;
|
||||
line-height: 2vw;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.clockContainer {
|
||||
.clock-container {
|
||||
grid-area: time;
|
||||
border-radius: 1vw;
|
||||
}
|
||||
@@ -132,17 +131,17 @@
|
||||
padding: 0.5vh 0 0.5vh 1vw;
|
||||
}
|
||||
|
||||
.todayContainer {
|
||||
.today-container {
|
||||
margin-top: 3vh;
|
||||
height: 95%;
|
||||
|
||||
.todayHeaderBlock {
|
||||
.today-header-block {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5vh;
|
||||
|
||||
.navItem,
|
||||
.navItemSelected {
|
||||
.nav-item,
|
||||
.nav-item--selected {
|
||||
background-color: $bg-gray-700;
|
||||
width: 0.7vw;
|
||||
height: 0.7vw;
|
||||
@@ -166,7 +165,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
.info-container {
|
||||
display: grid;
|
||||
|
||||
grid-template-rows: 3vh minmax(0, 1fr);
|
||||
@@ -176,14 +175,14 @@
|
||||
'binf qr';
|
||||
gap: 0.5vw;
|
||||
|
||||
.infoMessages {
|
||||
.info-messages {
|
||||
grid-area: binf;
|
||||
font-size: 1.5vw;
|
||||
line-height: 2vw;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.clockContainer {
|
||||
.clock-container {
|
||||
grid-area: time;
|
||||
border-radius: 1vw;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
@use 'main' as *;
|
||||
|
||||
@mixin viewer-container {
|
||||
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;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@mixin viewer-event-title {
|
||||
font-size: 3vw;
|
||||
font-weight: 600;
|
||||
text-decoration: underline $ontime-pink 0.5vh;
|
||||
padding-top: 0.2vh;
|
||||
padding-left: 1vw;
|
||||
}
|
||||
|
||||
@mixin viewer-info-messages {
|
||||
font-size: 1.5vw;
|
||||
line-height: 2vw;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
////////////////////////////// Specifics
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
.infoContainer > div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nextContainer > div,
|
||||
.nowContainer > div {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.nowContainer,
|
||||
.nextContainer,
|
||||
.todayContainer {
|
||||
background-color: $bg-gray-1000;
|
||||
padding: 1vh 2vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clockContainer,
|
||||
.countdownContainer,
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
padding: 1vh 1vw;
|
||||
}
|
||||
|
||||
.publicContainer {
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.publicContainerHidden {
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.todayContainer,
|
||||
.infoContainer {
|
||||
background-color: $bg-gray-1100;
|
||||
padding: 2.5vh 2vw;
|
||||
}
|
||||
|
||||
.infoContainer,
|
||||
.todayContainer,
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
border-radius: 1vw;
|
||||
}
|
||||
|
||||
.nowContainer,
|
||||
.nextContainer {
|
||||
margin-left: -1vw;
|
||||
}
|
||||
|
||||
.qr {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
grid-area: qr;
|
||||
}
|
||||
|
||||
.clock {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 3vw;
|
||||
line-height: 3vw;
|
||||
text-align: center;
|
||||
letter-spacing: 0.25vw;
|
||||
color: $clocks;
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
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;
|
||||
}
|
||||
|
||||
.infoMessages {
|
||||
grid-area: binf;
|
||||
font-size: 1.5vw;
|
||||
line-height: 2vw;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.clockContainer {
|
||||
grid-area: time;
|
||||
border-radius: 1vw;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 3vw;
|
||||
line-height: 3vw;
|
||||
padding: 0.5vh 0 0.5vh 1vw;
|
||||
}
|
||||
|
||||
.todayContainer {
|
||||
margin-top: 3vh;
|
||||
height: 95%;
|
||||
|
||||
.todayHeaderBlock {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5vh;
|
||||
|
||||
.navItem,
|
||||
.navItemSelected {
|
||||
background-color: $bg-gray-700;
|
||||
width: 0.7vw;
|
||||
height: 0.7vw;
|
||||
border-radius: 0.35vw;
|
||||
}
|
||||
|
||||
.navItemSelected {
|
||||
background-color: $bg-gray-100;
|
||||
}
|
||||
|
||||
.nav {
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-bottom: 2.5vh;
|
||||
}
|
||||
|
||||
.nav > div {
|
||||
margin-left: 0.5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
display: grid;
|
||||
|
||||
grid-template-rows: 3vh minmax(0, 1fr);
|
||||
grid-template-columns: 3fr 1fr;
|
||||
grid-template-areas:
|
||||
'titl .'
|
||||
'binf qr';
|
||||
gap: 0.5vw;
|
||||
|
||||
.infoMessages {
|
||||
grid-area: binf;
|
||||
font-size: 1.5vw;
|
||||
line-height: 2vw;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.clockContainer {
|
||||
grid-area: time;
|
||||
border-radius: 1vw;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ services:
|
||||
- "127.0.0.1:8888:8888/udp"
|
||||
volumes:
|
||||
- "./ontime-db:/server/preloaded-db"
|
||||
- "./ontime-external:/server/external"
|
||||
environment:
|
||||
- TZ=Asia/Singapore
|
||||
restart: unless-stopped
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "1.7.3",
|
||||
"version": "1.8.0",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
+3
-3
@@ -48,15 +48,15 @@ app.options('*', cors());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
app.use('/uploads', express.static('uploads'));
|
||||
|
||||
// Implement route endpoints
|
||||
app.use('/events', eventsRouter);
|
||||
app.use('/event', eventRouter);
|
||||
app.use('/ontime', ontimeRouter);
|
||||
app.use('/playback', playbackRouter);
|
||||
|
||||
// serve react
|
||||
// serve static - css
|
||||
app.use('/external', express.static(join(__dirname, 'external')));
|
||||
// serve static - react
|
||||
app.use(express.static(join(__dirname, env === 'production' ? '../' : '../../', 'client/build')));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
|
||||
@@ -234,6 +234,34 @@ export const postSettings = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Get view Settings
|
||||
* @method GET
|
||||
*/
|
||||
export const getViewSettings = async (req, res) => {
|
||||
res.status(200).send({ ...data.views });
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Change view Settings
|
||||
* @method POST
|
||||
*/
|
||||
export const postViewSettings = async (req, res) => {
|
||||
if (!req.body) {
|
||||
res.status(400).send('No object found in request');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
data.views = {
|
||||
overrideStyles: req.body?.overrideStyles ?? data.views.overrideStyles,
|
||||
};
|
||||
await db.write();
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
res.status(400).send(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/ontime/info'
|
||||
// Returns ACK message
|
||||
export const postInfo = async (req, res) => {
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
:root {
|
||||
--background-color-override: #f3f4f8;
|
||||
--color-override: #5051f9;
|
||||
--accent-color-override: #23235f;
|
||||
--timer-color-override: #5051f9;
|
||||
--outdent-background-color-override: white;
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-family: "Comic Sans MS", serif !important;
|
||||
}
|
||||
@@ -15,6 +15,9 @@ export const dbModelv1 = {
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
views: {
|
||||
overrideStyles: false,
|
||||
},
|
||||
aliases: [],
|
||||
userFields: {
|
||||
user0: 'user0',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^6.14.2",
|
||||
"lowdb": "3.0.0",
|
||||
"multer": "^1.4.4",
|
||||
"nanoid": "^3.3.3",
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import express from 'express';
|
||||
import { uploadFile } from '../utils/upload.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
import {
|
||||
dbDownload,
|
||||
dbUpload,
|
||||
getInfo,
|
||||
postInfo,
|
||||
dbPathToUpload,
|
||||
getOSC,
|
||||
postOSC,
|
||||
getSettings,
|
||||
postSettings,
|
||||
dbUpload,
|
||||
getAliases,
|
||||
postAliases,
|
||||
poll,
|
||||
getInfo,
|
||||
getOSC,
|
||||
getSettings,
|
||||
getUserFields,
|
||||
getViewSettings,
|
||||
poll,
|
||||
postAliases,
|
||||
postInfo,
|
||||
postOSC,
|
||||
postSettings,
|
||||
postUserFields,
|
||||
postViewSettings,
|
||||
} from '../controllers/ontimeController.js';
|
||||
|
||||
import { viewValidator } from '../validation/ontimeValidator.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
// create route between controller and '/ontime/sync' endpoint
|
||||
router.get('/poll', poll);
|
||||
|
||||
@@ -35,6 +38,12 @@ router.get('/settings', getSettings);
|
||||
// create route between controller and '/ontime/settings' endpoint
|
||||
router.post('/settings', postSettings);
|
||||
|
||||
// create route between controller and '/ontime/views' endpoint
|
||||
router.get('/views', getViewSettings);
|
||||
|
||||
// create route between controller and '/ontime/views' endpoint
|
||||
router.post('/views', viewValidator, postViewSettings);
|
||||
|
||||
// create route between controller and '/ontime/aliases' endpoint
|
||||
router.get('/aliases', getAliases);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import jest from 'jest-mock';
|
||||
import { dbModelv1, dbModelv1 as dbModel } from '../../models/dataModel.js';
|
||||
import { isStringEmpty, parseExcel_v1, parseJson_v1, validateEvent_v1 } from '../parser.js';
|
||||
import { makeString, validateDuration } from '../parserUtils.js';
|
||||
import { parseAliases_v1, parseUserFields_v1 } from '../parserUtils_v1.js';
|
||||
import { parseAliases_v1, parseUserFields_v1, parseViews_v1 } from '../parserUtils_v1.js';
|
||||
|
||||
describe('test json parser with valid def', () => {
|
||||
const testData = {
|
||||
@@ -817,6 +817,35 @@ describe('test userFields import', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('test views import', () => {
|
||||
it('imports data from file', () => {
|
||||
const testData = {
|
||||
events: [],
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: 1,
|
||||
},
|
||||
views: {
|
||||
overrideStyles: true,
|
||||
},
|
||||
};
|
||||
const parsed = parseViews_v1(testData);
|
||||
expect(parsed).toStrictEqual(testData.views);
|
||||
});
|
||||
|
||||
it('imports defaults to model', () => {
|
||||
const testData = {
|
||||
events: [],
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
const parsed = parseViews_v1(testData, true);
|
||||
expect(parsed).toStrictEqual(dbModelv1.views);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test validateDuration()', () => {
|
||||
describe('handles valid inputs', () => {
|
||||
const valid = [
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
parseOsc_v1,
|
||||
parseSettings_v1,
|
||||
parseUserFields_v1,
|
||||
parseViews_v1,
|
||||
} from './parserUtils_v1.js';
|
||||
import { parseExcelDate } from './time.js';
|
||||
import { generateId } from './generate_id.js';
|
||||
@@ -272,6 +273,8 @@ export const parseJson_v1 = async (jsonData, enforce = false) => {
|
||||
returnData.event = parseEvent_v1(jsonData, enforce);
|
||||
// Settings handled partially
|
||||
returnData.settings = parseSettings_v1(jsonData, enforce);
|
||||
// View settings handled partially
|
||||
returnData.views = parseViews_v1(jsonData, enforce);
|
||||
// Import OSC settings if any
|
||||
returnData.osc = parseOsc_v1(jsonData, enforce);
|
||||
// Import HTTP settings if any
|
||||
|
||||
@@ -118,6 +118,33 @@ export const parseSettings_v1 = (data, enforce) => {
|
||||
return newSettings;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse settings portion of an entry
|
||||
* @param {object} data - data object
|
||||
* @param {boolean} enforce - whether to create a definition if one is missing
|
||||
* @returns {object} - event object data
|
||||
*/
|
||||
export const parseViews_v1 = (data, enforce) => {
|
||||
let newViews = {};
|
||||
if ('views' in data) {
|
||||
console.log('Found view definition, importing...');
|
||||
const v = data.views;
|
||||
|
||||
const viewSettings = {
|
||||
overrideStyles: v.overrideStyles ?? dbModelv1.views.overrideStyles,
|
||||
};
|
||||
|
||||
// write to db
|
||||
newViews = {
|
||||
...viewSettings,
|
||||
};
|
||||
} else if (enforce) {
|
||||
newViews = dbModelv1.views;
|
||||
console.log(`Created view object in db`);
|
||||
}
|
||||
return newViews;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse osc portion of an entry
|
||||
* @param {object} data - data object
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { check, validationResult } from 'express-validator';
|
||||
|
||||
/**
|
||||
* @description Validates object for POST /ontime/views
|
||||
*/
|
||||
export const viewValidator = [
|
||||
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
@@ -559,6 +559,14 @@ express-session@^1.17.3:
|
||||
safe-buffer "5.2.1"
|
||||
uid-safe "~2.1.5"
|
||||
|
||||
express-validator@^6.14.2:
|
||||
version "6.14.2"
|
||||
resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.14.2.tgz#6147893f7bec0e14162c3a88b3653121afc4678f"
|
||||
integrity sha512-8XfAUrQ6Y7dIIuy9KcUPCfG/uCbvREctrxf5EeeME+ulanJ4iiW71lWmm9r4YcKKYOCBMan0WpVg7FtHu4Z4Wg==
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
validator "^13.7.0"
|
||||
|
||||
express@^4.18.1:
|
||||
version "4.18.1"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf"
|
||||
@@ -837,6 +845,11 @@ lodash.merge@^4.6.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lowdb@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-3.0.0.tgz#c10ab4e7eb86f1cbe255e35e60ffb0c6f42049e0"
|
||||
@@ -1420,6 +1433,11 @@ v8-compile-cache@^2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
|
||||
|
||||
validator@^13.7.0:
|
||||
version "13.7.0"
|
||||
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
|
||||
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
|
||||
|
||||
vary@^1, vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
|
||||
Reference in New Issue
Block a user