diff --git a/apps/client/src/common/components/state/Empty.tsx b/apps/client/src/common/components/state/Empty.tsx index e44146b84..08662b034 100644 --- a/apps/client/src/common/components/state/Empty.tsx +++ b/apps/client/src/common/components/state/Empty.tsx @@ -1,18 +1,20 @@ import { CSSProperties } from 'react'; import EmptyImage from '../../../assets/images/empty.svg?react'; +import { cx } from '../../utils/styleUtils'; import style from './Empty.module.scss'; interface EmptyProps { text?: string; style?: CSSProperties; + className?: string; } export default function Empty(props: EmptyProps) { - const { text, ...rest } = props; + const { text, className, ...rest } = props; return ( -
+
{text && {text}}
diff --git a/apps/client/src/common/components/title-card/TitleCard.scss b/apps/client/src/common/components/title-card/TitleCard.scss index 73a9de8b4..d2bfc598e 100644 --- a/apps/client/src/common/components/title-card/TitleCard.scss +++ b/apps/client/src/common/components/title-card/TitleCard.scss @@ -1,8 +1,5 @@ @use '../../../theme/viewerDefs' as *; -$title-primary-size: clamp(1.5rem, 3vw, 3rem); -$title-secondary-size: clamp(1rem, 2vw, 2.25rem); - .title-card { position: relative; display: flex; @@ -10,21 +7,23 @@ $title-secondary-size: clamp(1rem, 2vw, 2.25rem); } .title-card__title, -.title-card__secondary { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.title-card__title { +.title-card__placeholder { font-weight: 600; - font-size: $title-primary-size; - color: var(--color-override, $viewer-color); + font-size: $title-font-size; line-height: 1.2em; } +.title-card__title { + color: var(--color-override, $viewer-color); + padding-right: 1em; +} + +.title-card__placeholder { + color: var(--label-color-override, $viewer-label-color); +} + .title-card__secondary { - font-size: $title-secondary-size; + font-size: $base-font-size; color: var(--secondary-color-override, $viewer-secondary-color); line-height: 1.2em; } @@ -35,7 +34,6 @@ $title-secondary-size: clamp(1rem, 2vw, 2.25rem); top: 0.5rem; font-size: $timer-label-size; color: var(--secondary-color-override, $viewer-secondary-color); - margin-left: auto; text-transform: uppercase; &--accent { diff --git a/apps/client/src/common/components/title-card/TitleCard.tsx b/apps/client/src/common/components/title-card/TitleCard.tsx index 6613f895b..d2f773305 100644 --- a/apps/client/src/common/components/title-card/TitleCard.tsx +++ b/apps/client/src/common/components/title-card/TitleCard.tsx @@ -1,6 +1,7 @@ import { ForwardedRef, forwardRef } from 'react'; import { useTranslation } from '../../../translation/TranslationProvider'; +import { cx } from '../../utils/styleUtils'; import './TitleCard.scss'; @@ -18,9 +19,9 @@ const TitleCard = forwardRef((props: TitleCardProps, ref: ForwardedRef +
{title} - + {label && getLocalizedString(`common.${label}`)}
{secondary}
diff --git a/apps/client/src/common/components/view-logo/ViewLogo.tsx b/apps/client/src/common/components/view-logo/ViewLogo.tsx index 1988d485f..6ddde4593 100644 --- a/apps/client/src/common/components/view-logo/ViewLogo.tsx +++ b/apps/client/src/common/components/view-logo/ViewLogo.tsx @@ -7,5 +7,5 @@ interface ViewLogoProps { export default function ViewLogo(props: ViewLogoProps) { const { name, className } = props; - return ; + return ; } diff --git a/apps/client/src/common/hooks-query/useRundown.ts b/apps/client/src/common/hooks-query/useRundown.ts index d12398691..67629d4d9 100644 --- a/apps/client/src/common/hooks-query/useRundown.ts +++ b/apps/client/src/common/hooks-query/useRundown.ts @@ -1,6 +1,6 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { NormalisedRundown, OntimeRundown, RundownCached } from 'ontime-types'; +import { NormalisedRundown, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types'; import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { RUNDOWN } from '../api/constants'; @@ -11,6 +11,9 @@ import useProjectData from './useProjectData'; // revision is -1 so that the remote revision is higher const cachedRundownPlaceholder = { order: [] as string[], rundown: {} as NormalisedRundown, revision: -1 }; +/** + * Normalised rundown data + */ export default function useRundown() { const { data, status, isError, refetch, isFetching } = useQuery({ queryKey: RUNDOWN, @@ -24,6 +27,10 @@ export default function useRundown() { return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching }; } +/** + * Provides access to a flat rundown + * built from the order and rundown fields + */ export function useFlatRundown() { const { data, status } = useRundown(); const { data: projectData } = useProjectData(); @@ -52,3 +59,15 @@ export function useFlatRundown() { return { data: flatRunDown, status }; } + +/** + * Provides access to a partial rundown based on a filter callback + */ +export function usePartialRundown(cb: (event: OntimeRundownEntry) => boolean) { + const { data, status } = useFlatRundown(); + const filteredData = useMemo(() => { + return data.filter(cb); + }, [data, cb]); + + return { data: filteredData, status }; +} diff --git a/apps/client/src/common/hooks/useInterval.js b/apps/client/src/common/hooks/useInterval.js deleted file mode 100644 index dd25a8585..000000000 --- a/apps/client/src/common/hooks/useInterval.js +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useRef } from 'react'; - -/** - * @description utility hook to around setInterval - * @param callback - * @param delay - */ -export const useInterval = (callback, delay) => { - const savedCallback = useRef(); - - useEffect(() => { - savedCallback.current = callback; - }, [callback]); - - useEffect(() => { - /** - * @description function to be called - */ - function tick() { - savedCallback.current(); - } - if (delay !== null) { - const id = setInterval(tick, delay); - return () => clearInterval(id); - } - }, [delay]); -}; diff --git a/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.scss b/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.scss index 408df7264..0ccc50f6b 100644 --- a/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.scss +++ b/apps/client/src/features/viewers/common/superscript-time/SuperscriptTime.scss @@ -1,6 +1,6 @@ sup.period { top: -1em; - font-size: 0.4em; + font-size: 0.5em; } .subscript { diff --git a/apps/client/src/features/viewers/public/Public.tsx b/apps/client/src/features/viewers/public/Public.tsx index c2c08eb2f..db61f8645 100644 --- a/apps/client/src/features/viewers/public/Public.tsx +++ b/apps/client/src/features/viewers/public/Public.tsx @@ -10,9 +10,7 @@ import { useWindowTitle } from '../../../common/hooks/useWindowTitle'; import { ViewExtendedTimer } from '../../../common/models/TimeManager.type'; import { formatTime, getDefaultFormat } from '../../../common/utils/time'; import { useTranslation } from '../../../translation/TranslationProvider'; -import Schedule from '../../../views/common/schedule/Schedule'; -import { ScheduleProvider } from '../../../views/common/schedule/ScheduleContext'; -import ScheduleNav from '../../../views/common/schedule/ScheduleNav'; +import PublicSchedule from '../../../views/common/schedule/PublicSchedule'; import { titleVariants } from '../common/animation'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { getPropertyValue } from '../common/viewUtils'; @@ -30,23 +28,13 @@ interface BackstageProps { publicEventNow: OntimeEvent | null; publicEventNext: OntimeEvent | null; time: ViewExtendedTimer; - events: OntimeEvent[]; publicSelectedId: string | null; settings: Settings | undefined; } export default function Public(props: BackstageProps) { - const { - customFields, - general, - isMirrored, - publicEventNow, - publicEventNext, - time, - events, - publicSelectedId, - settings, - } = props; + const { customFields, general, isMirrored, publicEventNow, publicEventNext, time, publicSelectedId, settings } = + props; const { getLocalizedString } = useTranslation(); const [searchParams] = useSearchParams(); @@ -109,10 +97,7 @@ export default function Public(props: BackstageProps) {
- - - - +
{general.publicUrl && } diff --git a/apps/client/src/theme/_viewerDefs.scss b/apps/client/src/theme/_viewerDefs.scss index 71d37489d..a8a8a1b8b 100644 --- a/apps/client/src/theme/_viewerDefs.scss +++ b/apps/client/src/theme/_viewerDefs.scss @@ -9,6 +9,12 @@ $viewer-transition-time: 0.5s; $viewer-font-family: 'Open Sans', 'Segoe UI', sans-serif; // --font-family-override $viewer-opacity-disabled: 0.6; +$timer-label-size: clamp(12px, 1.25vw, 20px); +$base-font-size: clamp(15px, 1.5vw, 28px); +$title-font-size: clamp(18px, 2.25vw, 42px); +$timer-value-size: clamp(24px, 2.5vw, 48px); +$header-font-size: clamp(28px, 3.5vw, 64px); + // General styling $accent-color: $red-500; // --accent-color-override $delay-color: $ontime-delay-text; @@ -23,16 +29,15 @@ $element-border-radius: 8px; // Generic element sizes $view-element-gap: min(2vh, 16px); +$view-block-padding: min(2vh, 16px); +$view-inline-padding: clamp(16px, 2vw, 24px); $view-outer-padding: min(2vh, 16px) clamp(16px, 2vw, 24px); + $view-card-padding: min(2vh, 8px) clamp(16px, 2vw, 24px); // Properties related to timer $timer-color: rgba(white, 80%); // --timer-color-override $timer-finished-color: $playback-negative; -$timer-bold-font-family: 'Arial Black', sans-serif; // --card-background-color-override +$timer-bold-font-family: 'Arial Black', sans-serif; // --font-family-bold-override $external-color: rgba(white, 85%); // --external-color-override - -// properties of other timers (clock and countdown) -$timer-label-size: clamp(16px, 1.5vw, 24px); -$timer-value-size: clamp(32px, 3.5vw, 50px); diff --git a/apps/client/src/translation/languages/de.ts b/apps/client/src/translation/languages/de.ts index e75c293aa..03e6837eb 100644 --- a/apps/client/src/translation/languages/de.ts +++ b/apps/client/src/translation/languages/de.ts @@ -13,6 +13,7 @@ export const langDe: TranslationObject = { 'common.stage_timer': 'Bühnen-Timer', 'common.started_at': 'Gestartet am', 'common.time_now': 'Aktuelle Zeit', + 'common.no_data': 'Keine Daten', 'countdown.ended': 'Veranstaltung endete um', 'countdown.running': 'Veranstaltung läuft', 'countdown.select_event': 'Wählen Sie eine Veranstaltung aus, um sie zu verfolgen', diff --git a/apps/client/src/translation/languages/en.ts b/apps/client/src/translation/languages/en.ts index c0889b05b..9dfa13347 100644 --- a/apps/client/src/translation/languages/en.ts +++ b/apps/client/src/translation/languages/en.ts @@ -11,6 +11,7 @@ export const langEn = { 'common.stage_timer': 'Stage Timer', 'common.started_at': 'Started At', 'common.time_now': 'Time now', + 'common.no_data': 'No data', 'countdown.ended': 'Event ended at', 'countdown.running': 'Event running', 'countdown.select_event': 'Select an event to follow', diff --git a/apps/client/src/translation/languages/es.ts b/apps/client/src/translation/languages/es.ts index e63a30c4b..dac98ce31 100644 --- a/apps/client/src/translation/languages/es.ts +++ b/apps/client/src/translation/languages/es.ts @@ -13,6 +13,7 @@ export const langEs: TranslationObject = { 'common.stage_timer': 'Temporizador de presentador', 'common.started_at': 'Iniciado en', 'common.time_now': 'Ahora', + 'common.no_data': 'Sin datos', 'countdown.ended': 'Evento finalizado a las', 'countdown.running': 'Evento en curso', 'countdown.select_event': 'Seleccionar un evento para seguir', diff --git a/apps/client/src/translation/languages/fr.ts b/apps/client/src/translation/languages/fr.ts index 5ed6eab3f..521ccda92 100644 --- a/apps/client/src/translation/languages/fr.ts +++ b/apps/client/src/translation/languages/fr.ts @@ -13,6 +13,7 @@ export const langFr: TranslationObject = { 'common.stage_timer': 'Minuteur de scène', 'common.started_at': 'Commencé à', 'common.time_now': 'Heure', + 'common.no_data': 'Aucune donnée', 'countdown.ended': 'Évènement terminé à', 'countdown.running': 'Évènement en cours', 'countdown.select_event': 'Sélectionnez un évènement à suivre', diff --git a/apps/client/src/translation/languages/hu.ts b/apps/client/src/translation/languages/hu.ts index 46669cbbe..542f31888 100644 --- a/apps/client/src/translation/languages/hu.ts +++ b/apps/client/src/translation/languages/hu.ts @@ -13,6 +13,7 @@ export const langHu: TranslationObject = { 'common.stage_timer': 'Színpadi időzítő', 'common.started_at': 'Kezdődött', 'common.time_now': 'Jelenlegi idő', + 'common.no_data': 'Nincsenek adatok', 'countdown.ended': 'Esemény véget ért', 'countdown.running': 'Esemény folyamatban', 'countdown.select_event': 'Válassza ki a követendő eseményt', diff --git a/apps/client/src/translation/languages/it.ts b/apps/client/src/translation/languages/it.ts index 830425c4b..41244fb7a 100644 --- a/apps/client/src/translation/languages/it.ts +++ b/apps/client/src/translation/languages/it.ts @@ -13,6 +13,7 @@ export const langIt: TranslationObject = { 'common.stage_timer': 'Orologio Palco', 'common.started_at': 'Iniziato Alle', 'common.time_now': 'Ora attuale', + 'common.no_data': 'Nessun dato disponibile', 'countdown.ended': 'Evento finito alle', 'countdown.running': 'Evento in corso', 'countdown.select_event': 'Seleziona un evento da seguire', diff --git a/apps/client/src/translation/languages/no.ts b/apps/client/src/translation/languages/no.ts index 075aa142e..455623d7c 100644 --- a/apps/client/src/translation/languages/no.ts +++ b/apps/client/src/translation/languages/no.ts @@ -13,6 +13,7 @@ export const langNo: TranslationObject = { 'common.stage_timer': 'Scenetimer', 'common.started_at': 'Startet', 'common.time_now': 'Klokken nå', + 'common.no_data': 'Ingen data tilgjengelig', 'countdown.ended': 'Hendelse avsluttet', 'countdown.running': 'Hendelse pågår', 'countdown.select_event': 'Velg en hendelse å følge', diff --git a/apps/client/src/translation/languages/pl.ts b/apps/client/src/translation/languages/pl.ts index a974c1827..6b8b30e4f 100644 --- a/apps/client/src/translation/languages/pl.ts +++ b/apps/client/src/translation/languages/pl.ts @@ -13,6 +13,7 @@ export const langPl: TranslationObject = { 'common.stage_timer': 'Timer Scena', 'common.started_at': 'Rozpoczęte o', 'common.time_now': 'Aktualny czas', + 'common.no_data': 'Brak danych', 'countdown.ended': 'Zakończone o', 'countdown.running': 'Trwa', 'countdown.select_event': 'Wybierz event który chcesz śledzić', diff --git a/apps/client/src/translation/languages/pt.ts b/apps/client/src/translation/languages/pt.ts index fdae87c28..e58f425fd 100644 --- a/apps/client/src/translation/languages/pt.ts +++ b/apps/client/src/translation/languages/pt.ts @@ -13,6 +13,7 @@ export const langPt: TranslationObject = { 'common.stage_timer': 'Temporizador do presentador', 'common.started_at': 'Iniciado em', 'common.time_now': 'Hora atual', + 'common.no_data': 'Sem dados', 'countdown.ended': 'Evento encerrado às', 'countdown.running': 'Evento em andamento', 'countdown.select_event': 'Selecione um evento para acompanhar', diff --git a/apps/client/src/translation/languages/sv.ts b/apps/client/src/translation/languages/sv.ts index 8fc9c9fb7..2187d9eab 100644 --- a/apps/client/src/translation/languages/sv.ts +++ b/apps/client/src/translation/languages/sv.ts @@ -13,6 +13,7 @@ export const langSv: TranslationObject = { 'common.stage_timer': 'Timer för scenen', 'common.started_at': 'Började vid', 'common.time_now': 'Klockan nu', + 'common.no_data': 'Inga tillgängliga data', 'countdown.ended': 'Evenemanget avslutades vid', 'countdown.running': 'Evenemang pågår', 'countdown.select_event': 'Välj ett evenemang att följa', diff --git a/apps/client/src/translation/languages/zh.ts b/apps/client/src/translation/languages/zh.ts index 13345438c..116f2b600 100644 --- a/apps/client/src/translation/languages/zh.ts +++ b/apps/client/src/translation/languages/zh.ts @@ -13,6 +13,7 @@ export const langZhCn: TranslationObject = { 'common.stage_timer': '舞台计时器', 'common.started_at': '开始于', 'common.time_now': '当前时间', + 'common.no_data': '无数据', 'countdown.ended': '事件结束于', 'countdown.running': '事件进行中', 'countdown.select_event': '选择要关注的事件', diff --git a/apps/client/src/views/backstage/Backstage.scss b/apps/client/src/views/backstage/Backstage.scss index d359a2c2a..4955104bc 100644 --- a/apps/client/src/views/backstage/Backstage.scss +++ b/apps/client/src/views/backstage/Backstage.scss @@ -10,23 +10,31 @@ font-family: var(--font-family-override, $viewer-font-family); background: var(--background-color-override, $viewer-background-color); color: var(--color-override, $viewer-color); - gap: min(2vh, 16px); - padding: min(2vh, 16px) clamp(16px, 10vw, 64px); + gap: $view-element-gap; + padding: $view-outer-padding; display: grid; - grid-template-columns: 1fr 1fr 40vw; + grid-template-columns: 1fr 40vw; grid-template-rows: auto 12px 1fr auto; grid-template-areas: - ' header header header' - ' progress progress schedule-nav' - ' now now schedule' - ' info info schedule'; + 'header header' + 'progress schedule-nav' + 'card schedule' + 'info schedule'; + + .empty-container { + position: absolute; + top: 0; + left: 0; + right: 0; + margin-top: 25vh; + } /* =================== HEADER + EXTRAS ===================*/ .project-header { grid-area: header; - font-size: clamp(32px, 4.5vw, 64px); + font-size: $header-font-size; font-weight: 600; display: flex; gap: 1rem; @@ -37,19 +45,22 @@ max-height: min(100px, 20vh); } + .title { + line-height: 1.1em; + } + .clock-container { margin-left: auto; + font-weight: 600; .label { - font-size: clamp(16px, 1.5vw, 24px); - font-weight: 600; + font-size: $timer-label-size; color: var(--label-color-override, $viewer-label-color); text-transform: uppercase; } .time { - font-size: clamp(32px, 3.5vw, 50px); - font-weight: 600; + font-size: $timer-value-size; color: var(--secondary-color-override, $viewer-secondary-color); letter-spacing: 0.05em; line-height: 0.95em; @@ -64,17 +75,27 @@ margin: 0 auto -8px; } - .now-container { - grid-area: now; + .card-container { + grid-area: card; display: flex; flex-direction: column; - gap: min(2vh, 16px); + gap: $view-element-gap; + } + + .empty { + font-size: clamp(24px, 2vw, 32px); + width: 100%; + height: 100%; + display: grid; + place-content: center; + text-align: center; + color: var(--label-color-override, $viewer-label-color); } .event { background-color: var(--card-background-color-override, $viewer-card-bg-color); - padding: 16px 24px; - border-radius: 8px; + padding: $view-card-padding; + border-radius: $element-border-radius; &.blink { animation-name: blink; @@ -87,9 +108,10 @@ .timer-group { grid-area: timer; border-top: 2px solid var(--background-color-override, $viewer-background-color); - margin-top: 2em; - padding-top: 1em; + margin-top: max(1vh, 16px); + padding-top: max(1vh, 16px); display: flex; + row-gap: 0.5em; } .timer-gap { @@ -98,58 +120,106 @@ } .aux-timers { - font-size: max(1vw, 16px); - &__label { + font-size: $timer-label-size; color: var(--label-color-override, $viewer-label-color); font-weight: 600; text-transform: uppercase; } &__value { + font-size: $base-font-size; color: var(--secondary-color-override, $viewer-secondary-color); - font-size: clamp(24px, 2vw, 32px); letter-spacing: 0.05em; + line-height: 0.95em; + } + + &--pending { + color: var(--timer-pending-color-override, $ontime-roll); } } /* =================== MAIN - SCHEDULE ===================*/ + $schedule-left-spacing: clamp(16px, 4vw, 64px); .schedule-container { grid-area: schedule; overflow: hidden; height: 100%; - margin-left: clamp(16px, 5vw, 64px); + padding-left: $schedule-left-spacing; } .schedule-nav-container { grid-area: schedule-nav; - align-self: center; + padding-left: $schedule-left-spacing; } + /* =================== MAIN - INFO ===================*/ + .info { grid-area: info; display: flex; gap: max(1vw, 16px); align-self: flex-end; overflow: hidden; + align-items: end; &__message { - font-size: clamp(16px, 1.5vw, 24px); - line-height: 1.3em; + font-size: $base-font-size; + line-height: 1.2em; white-space: pre-line; overflow: hidden; flex: 1; } .qr { - padding: 4px; - background-color: white; + padding: 0.5rem; + background-color: $ui-white; + border-radius: 2px; } } } -/* =================== AMIMATION ===================*/ +/* =================== MOBILE ===================*/ +@media screen and (max-width: 768px) { + .backstage { + display: flex; + flex-direction: column; + overflow-y: auto; + + .project-header { + flex-direction: column; + position: relative; + gap: 0.5rem; + } + + .logo { + height: min(50px, 10vh); + } + + .timer-group { + flex-wrap: wrap; + } + + .clock-container { + position: absolute; + top: 0; + right: 0; + } + + .schedule-container { + margin-left: 0; + flex: 1; + } + + .info { + flex: 1; + width: 100%; + } + } +} + +/* =================== ANIMATION ===================*/ @keyframes blink { 0% { diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index af44661f5..eb89b66c5 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -1,32 +1,27 @@ import { useEffect, useState } from 'react'; import QRCode from 'react-qr-code'; -import { useSearchParams } from 'react-router-dom'; -import { AnimatePresence, motion } from 'framer-motion'; -import { CustomFields, OntimeEvent, ProjectData, Settings, SupportedEvent } from 'ontime-types'; +import { useViewportSize } from '@mantine/hooks'; +import { CustomFields, OntimeEvent, ProjectData, Runtime, Settings } from 'ontime-types'; import { millisToString, removeLeadingZero } from 'ontime-utils'; import ProgressBar from '../../common/components/progress-bar/ProgressBar'; +import Empty from '../../common/components/state/Empty'; import TitleCard from '../../common/components/title-card/TitleCard'; import ViewLogo from '../../common/components/view-logo/ViewLogo'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; import { useWindowTitle } from '../../common/hooks/useWindowTitle'; import { ViewExtendedTimer } from '../../common/models/TimeManager.type'; -import { timerPlaceholderMin } from '../../common/utils/styleUtils'; +import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils'; import { formatTime, getDefaultFormat } from '../../common/utils/time'; import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime'; -import { getPropertyValue } from '../../features/viewers/common/viewUtils'; import { useTranslation } from '../../translation/TranslationProvider'; -import Schedule from '../common/schedule/Schedule'; -import { ScheduleProvider } from '../common/schedule/ScheduleContext'; -import ScheduleNav from '../common/schedule/ScheduleNav'; -import { titleVariants } from '../timer/timer.animations'; +import BackstageSchedule from '../common/schedule/BackstageSchedule'; -import { getBackstageOptions } from './backstage.options'; +import { getBackstageOptions, useBackstageOptions } from './backstage.options'; +import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from './backstage.utils'; import './Backstage.scss'; -export const MotionTitleCard = motion(TitleCard); - interface BackstageProps { backstageEvents: OntimeEvent[]; customFields: CustomFields; @@ -35,16 +30,29 @@ interface BackstageProps { general: ProjectData; isMirrored: boolean; time: ViewExtendedTimer; + runtime: Runtime; selectedId: string | null; settings: Settings | undefined; } export default function Backstage(props: BackstageProps) { - const { backstageEvents, customFields, eventNext, eventNow, general, time, isMirrored, selectedId, settings } = props; + const { + backstageEvents, + customFields, + eventNext, + eventNow, + general, + time, + isMirrored, + runtime, + selectedId, + settings, + } = props; const { getLocalizedString } = useTranslation(); + const { secondarySource } = useBackstageOptions(); const [blinkClass, setBlinkClass] = useState(false); - const [searchParams] = useSearchParams(); + const { height: screenHeight } = useViewportSize(); useWindowTitle('Backstage'); @@ -59,22 +67,31 @@ export default function Backstage(props: BackstageProps) { return () => clearTimeout(timer); }, [selectedId]); + // gather card data + const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData( + eventNow, + eventNext, + 'title', + secondarySource, + time.playback, + ); + + // gather timer data const clock = formatTime(time.clock); - const startedAt = formatTime(time.startedAt); - const isNegative = (time.current ?? 0) < 0; - const expectedFinish = isNegative ? getLocalizedString('countdown.overtime') : formatTime(time.expectedFinish); - - const qrSize = Math.max(window.innerWidth / 15, 128); - const filteredEvents = backstageEvents.filter((event) => event.type === SupportedEvent.Event); - const showProgress = time.playback !== 'stop'; - - const secondarySource = searchParams.get('secondary-src'); - const secondaryTextNext = getPropertyValue(eventNext, secondarySource); - const secondaryTextNow = getPropertyValue(eventNow, secondarySource); + const isPendingStart = getIsPendingStart(time.playback, time.phase); + const startedAt = isPendingStart ? formatTime(time.secondaryTimer) : formatTime(time.startedAt); + const scheduledStart = showNow ? '' : formatTime(runtime.plannedStart, { format12: 'hh:mm a', format24: 'HH:mm' }); + const scheduledEnd = showNow ? '' : formatTime(runtime.plannedEnd, { format12: 'hh:mm a', format24: 'HH:mm' }); let stageTimer = millisToString(time.current, { fallback: timerPlaceholderMin }); stageTimer = removeLeadingZero(stageTimer); + // gather presentation styles + const qrSize = Math.max(window.innerWidth / 15, 72); + const showProgress = getShowProgressBar(time.playback); + const showSchedule = screenHeight > 700; // in vertical screens we may not have space + + // gather option data const defaultFormat = getDefaultFormat(settings?.timeFormat); const backstageOptions = getBackstageOptions(defaultFormat, customFields); @@ -82,8 +99,8 @@ export default function Backstage(props: BackstageProps) {
- {general?.projectLogo && } - {general.title} + {general?.projectLogo ? :
} +
{general.title}
{getLocalizedString('common.time_now')}
@@ -97,63 +114,62 @@ export default function Backstage(props: BackstageProps) { hidden={!showProgress} /> -
- - {eventNow && ( - - -
-
-
{getLocalizedString('common.started_at')}
- -
-
-
-
{getLocalizedString('common.expected_finish')}
- {isNegative ? ( -
{expectedFinish}
- ) : ( - - )} -
-
-
-
{getLocalizedString('common.stage_timer')}
-
{stageTimer}
-
-
- - )} - + {backstageEvents.length === 0 && ( +
+ +
+ )} - - {eventNext && ( - - )} - +
+ {showNow ? ( +
+ +
+
+
+ {isPendingStart ? getLocalizedString('countdown.waiting') : getLocalizedString('common.started_at')} +
+ +
+
+
+
{getLocalizedString('common.expected_finish')}
+ {isOvertime(time.current) ? ( +
{getLocalizedString('countdown.overtime')}
+ ) : ( + + )} +
+
+
+
{getLocalizedString('common.stage_timer')}
+
{stageTimer}
+
+
+
+ ) : ( +
+
{getLocalizedString('countdown.waiting')}
+
+
+
+ {getLocalizedString('common.scheduled_start')} +
+ +
+
+
+
{getLocalizedString('common.scheduled_end')}
+ +
+
+
+ )} + + {showNext && }
- - - - + {showSchedule && }
{general.backstageUrl && } diff --git a/apps/client/src/views/backstage/backstage.options.ts b/apps/client/src/views/backstage/backstage.options.ts index 620bc81fe..ccf4f94e3 100644 --- a/apps/client/src/views/backstage/backstage.options.ts +++ b/apps/client/src/views/backstage/backstage.options.ts @@ -1,4 +1,6 @@ -import { CustomFields } from 'ontime-types'; +import { useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { CustomFields, OntimeEvent } from 'ontime-types'; import { getTimeOption, @@ -6,6 +8,7 @@ import { OptionTitle, } from '../../common/components/view-params-editor/constants'; import { ViewOption } from '../../common/components/view-params-editor/types'; +import { isStringBoolean } from '../../features/viewers/common/viewUtils'; export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' }); @@ -26,25 +29,10 @@ export const getBackstageOptions = (timeFormat: string, customFields: CustomFiel }, ], }, - { title: OptionTitle.Schedule, collapsible: true, options: [ - { - id: 'eventsPerPage', - title: 'Events per page', - description: 'Sets the number of events on the page, can cause overflow', - type: 'number', - placeholder: '8 (default)', - }, - { - id: 'hidePast', - title: 'Hide past events', - description: 'Scheduler will only show upcoming events', - type: 'boolean', - defaultValue: false, - }, { id: 'stopCycle', title: 'Stop cycling through event pages', @@ -52,7 +40,59 @@ export const getBackstageOptions = (timeFormat: string, customFields: CustomFiel type: 'boolean', defaultValue: false, }, + { + id: 'cycleInterval', + title: 'Cycle interval', + description: 'How long (in seconds) should each schedule page be shown.', + type: 'number', + defaultValue: 10, + }, ], }, ]; }; + +type BackstageOptions = { + secondarySource: keyof OntimeEvent | null; +}; + +/** + * Utility extract the view options from URL Params + * the names and fallback are manually matched with timerOptions + */ +function getOptionsFromParams(searchParams: URLSearchParams): BackstageOptions { + // we manually make an object that matches the key above + return { + secondarySource: searchParams.get('secondary-src') as keyof OntimeEvent | null, + }; +} + +/** + * Hook exposes the backstage view options + */ +export function useBackstageOptions(): BackstageOptions { + const [searchParams] = useSearchParams(); + const options = useMemo(() => getOptionsFromParams(searchParams), [searchParams]); + return options; +} + +type ScheduleOptions = { + cycleInterval: number; + stopCycle: boolean; +}; + +function getScheduleOptionsFromParams(searchParams: URLSearchParams): ScheduleOptions { + return { + cycleInterval: Number(searchParams.get('cycleInterval')) || 10, + stopCycle: isStringBoolean(searchParams.get('stopCycle')), + }; +} + +/** + * Hook exposes the schedule component options + */ +export function useScheduleOptions() { + const [searchParams] = useSearchParams(); + const options = useMemo(() => getScheduleOptionsFromParams(searchParams), [searchParams]); + return options; +} diff --git a/apps/client/src/views/backstage/backstage.utils.ts b/apps/client/src/views/backstage/backstage.utils.ts new file mode 100644 index 000000000..03086b3c5 --- /dev/null +++ b/apps/client/src/views/backstage/backstage.utils.ts @@ -0,0 +1,61 @@ +import { MaybeNumber, OntimeEvent, Playback, TimerPhase } from 'ontime-types'; + +import { getPropertyValue } from '../../features/viewers/common/viewUtils'; + +/** + * Whether the current time is in overtime + */ +export function isOvertime(current: MaybeNumber): boolean { + return (current ?? 0) < 0; +} + +/** + * Whether the progress bar should be shown + */ +export function getShowProgressBar(playback: Playback): boolean { + return playback !== Playback.Stop; +} + +/** + * Whether the playback is pending start (ie: Roll mode waiting to start) + */ +export function getIsPendingStart(playback: Playback, phase: TimerPhase): boolean { + return playback === Playback.Roll && phase === TimerPhase.Pending; +} + +/** + * What should we be showing in the cards? + */ +export function getCardData( + eventNow: OntimeEvent | null, + eventNext: OntimeEvent | null, + mainSource: keyof OntimeEvent | null, + secondarySource: keyof OntimeEvent | null, + playback: Playback, +) { + if (playback === Playback.Stop) { + return { + showNow: false, + nowMain: undefined, + nowSecondary: undefined, + showNext: false, + nextMain: undefined, + nextSecondary: undefined, + }; + } + + // if we are loaded, we show the upcoming event as next + const nowMain = getPropertyValue(eventNow, mainSource ?? 'title'); + const nowSecondary = getPropertyValue(eventNow, secondarySource); + const nextMain = getPropertyValue(eventNext, mainSource ?? 'title'); + const nextSecondary = getPropertyValue(eventNext, secondarySource); + + return { + showNow: Boolean(nowMain) || Boolean(nowSecondary), + nowMain, + nowSecondary, + showNext: Boolean(nextMain) || Boolean(nextSecondary), + nextMain, + nextSecondary, + }; +} diff --git a/apps/client/src/views/common/schedule/BackstageSchedule.tsx b/apps/client/src/views/common/schedule/BackstageSchedule.tsx new file mode 100644 index 000000000..f29790b6b --- /dev/null +++ b/apps/client/src/views/common/schedule/BackstageSchedule.tsx @@ -0,0 +1,21 @@ +import { memo } from 'react'; +import { MaybeString } from 'ontime-types'; + +import Schedule from './Schedule'; +import { ScheduleProvider } from './ScheduleContext'; +import ScheduleNav from './ScheduleNav'; + +interface BackstageScheduleProps { + selectedId: MaybeString; +} + +export default memo(BackstageSchedule); +function BackstageSchedule(props: BackstageScheduleProps) { + const { selectedId } = props; + return ( + + + + + ); +} diff --git a/apps/client/src/views/common/schedule/PublicSchedule.tsx b/apps/client/src/views/common/schedule/PublicSchedule.tsx new file mode 100644 index 000000000..b6c16d815 --- /dev/null +++ b/apps/client/src/views/common/schedule/PublicSchedule.tsx @@ -0,0 +1,21 @@ +import { memo } from 'react'; +import { MaybeString } from 'ontime-types'; + +import Schedule from './Schedule'; +import { ScheduleProvider } from './ScheduleContext'; +import ScheduleNav from './ScheduleNav'; + +interface PublicScheduleProps { + selectedId: MaybeString; +} + +export default memo(PublicSchedule); +function PublicSchedule(props: PublicScheduleProps) { + const { selectedId } = props; + return ( + + + + + ); +} diff --git a/apps/client/src/views/common/schedule/Schedule.scss b/apps/client/src/views/common/schedule/Schedule.scss index c7239904c..ce83872f8 100644 --- a/apps/client/src/views/common/schedule/Schedule.scss +++ b/apps/client/src/views/common/schedule/Schedule.scss @@ -2,75 +2,82 @@ .schedule { width: 100%; - border-spacing: 50px; + position: relative; + list-style: none; .entry { - font-size: clamp(16px, 1.5vw, 24px); + position: absolute; + padding-bottom: 1em; - .entry-colour { - background-color: var(--card-background-color-override, $viewer-card-bg-color); - height: clamp(8px, 0.75vw, 12px); - width: clamp(8px, 0.75vw, 12px); - border-radius: 6px; - display: inline-block; + &--skip { + text-decoration: line-through; + text-decoration-color: var(--secondary-color-override, $viewer-secondary-color); } + } - .entry-times { - font-family: $viewer-font-family; - color: var(--secondary-color-override, $viewer-secondary-color); - font-weight: 300; - letter-spacing: 0.05em; + .entry-colour { + background-color: var(--card-background-color-override, $viewer-card-bg-color); + height: clamp(8px, 0.75vw, 12px); + width: clamp(8px, 0.75vw, 12px); + border-radius: 6px; + margin-right: 0.25rem; + } + + .entry-times { + color: var(--secondary-color-override, $viewer-secondary-color); + letter-spacing: 0.05em; + font-size: $timer-label-size; + display: flex; + align-items: center; + gap: 0.2rem; + + &--delayed, + &--delay { display: flex; align-items: center; - gap: 8px; + gap: 0.25rem; } - .entry-title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .entry-secondary { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &:not(:last-child) { - padding-bottom: 8px; - } - - &--past { - color: var(--secondary-color-override, $viewer-secondary-color); - } - - &--now { - .entry-title { - color: var(--accent-color-override, $accent-color); - font-weight: 600; - } - } - - &.skip { + &--delayed { text-decoration: line-through; + text-decoration-color: $ontime-delay; } + + &--delay { + color: $ontime-delay-text; + } + } + + .entry-title { + font-size: clamp(16px, 1.5vw, 24px); + font-size: $base-font-size; + line-height: 1.2em; } } .schedule-nav { display: flex; - justify-content: flex-end; .schedule-nav__item { - background-color: var(--card-background-color-override, $viewer-card-bg-color); - width: 12px; - height: 12px; + background-color: var(--color-override, $viewer-color); + opacity: 0.2; + + height: clamp(8px, 0.75vw, 12px); + width: clamp(8px, 0.75vw, 12px); border-radius: 6px; - margin-left: 8px; + margin-right: 8px; + + transition-property: opacity; + transition-duration: 1s; &--selected { - background-color: var(--color-override, $viewer-color); + transition-property: opacity; + transition-duration: $viewer-transition-time; + opacity: 1; + } + + &--indeterminate { + width: clamp(32px, 3vw, 48px); } } } diff --git a/apps/client/src/views/common/schedule/Schedule.tsx b/apps/client/src/views/common/schedule/Schedule.tsx index a7e394a5f..acb6d54d8 100644 --- a/apps/client/src/views/common/schedule/Schedule.tsx +++ b/apps/client/src/views/common/schedule/Schedule.tsx @@ -1,3 +1,6 @@ +import { cx } from '../../../common/utils/styleUtils'; + +import { getScheduledTimes } from './schedule.utils'; import { useSchedule } from './ScheduleContext'; import ScheduleItem from './ScheduleItem'; @@ -9,41 +12,27 @@ interface ScheduleProps { } export default function Schedule({ isProduction, className }: ScheduleProps) { - const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule(); + const { events, isBackstage, containerRef } = useSchedule(); - // TODO: design a nice placeholder for empty schedules - if (paginatedEvents?.length < 1) { + if (events?.length < 1) { return null; } - let selectedState: 'past' | 'now' | 'future' = 'past'; - return ( -
    - {paginatedEvents.map((event) => { - if (scheduleType === 'past' || scheduleType === 'future') { - selectedState = scheduleType; - } else { - if (event.id === selectedEventId) { - selectedState = 'now'; - } else if (selectedState === 'now') { - selectedState = 'future'; - } - } - - const timeStart = isProduction ? event.timeStart + (event?.delay ?? 0) : event.timeStart; - const timeEnd = isProduction ? event.timeEnd + (event?.delay ?? 0) : event.timeEnd; +
      + {events.map((event) => { + const { timeStart, timeEnd, delay } = getScheduledTimes(event, isProduction); return ( ); })} diff --git a/apps/client/src/views/common/schedule/ScheduleContext.tsx b/apps/client/src/views/common/schedule/ScheduleContext.tsx index 6c72a6621..a690c3d76 100644 --- a/apps/client/src/views/common/schedule/ScheduleContext.tsx +++ b/apps/client/src/views/common/schedule/ScheduleContext.tsx @@ -1,91 +1,155 @@ -import { createContext, PropsWithChildren, useContext, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { OntimeEvent } from 'ontime-types'; +import { + createContext, + PropsWithChildren, + RefObject, + useContext, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'react'; +import { isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types'; -import { useInterval } from '../../../common/hooks/useInterval'; -import { isStringBoolean } from '../../../features/viewers/common/viewUtils'; +import { usePartialRundown } from '../../../common/hooks-query/useRundown'; +import { useScheduleOptions } from '../../backstage/backstage.options'; interface ScheduleContextState { events: OntimeEvent[]; - paginatedEvents: OntimeEvent[]; selectedEventId: string | null; - scheduleType: 'past' | 'now' | 'future'; numPages: number; visiblePage: number; isBackstage: boolean; + containerRef: RefObject; } const ScheduleContext = createContext(undefined); interface ScheduleProviderProps { - events: OntimeEvent[]; selectedEventId: string | null; isBackstage?: boolean; - time?: number; } -const numEventsPerPage = 8; - export const ScheduleProvider = ({ children, - events, selectedEventId, isBackstage = false, - time = 10, }: PropsWithChildren) => { - const [visiblePage, setVisiblePage] = useState(0); - const [searchParams] = useSearchParams(); + const { cycleInterval, stopCycle } = useScheduleOptions(); + const { data: events } = usePartialRundown((event: OntimeRundownEntry) => { + if (isBackstage) { + return isOntimeEvent(event); + } + return isOntimeEvent(event) && event.isPublic && !event.skip; + }); - // look for overrides from views - const hidePast = isStringBoolean(searchParams.get('hidePast')); - const stopCycle = isStringBoolean(searchParams.get('stopCycle')); - const eventsPerPage = Number(searchParams.get('eventsPerPage') ?? numEventsPerPage); + const [firstIndex, setFirstIndex] = useState(-1); + const [numPages, setNumPages] = useState(0); + const [visiblePage, setVisiblePage] = useState(0); + + const lastIndex = useRef(-1); + const paginator = useRef(); + + const containerRef = useRef(null); + + useLayoutEffect(() => { + if (!containerRef.current) return; + + const children = Array.from(containerRef.current.children) as HTMLElement[]; + if (children.length === 0) { + return; + } + + const containerHeight = containerRef.current.clientHeight; + let currentPageHeight = 0; // used to check when we need to paginate + let currentPage = 1; + let numPages = 1; + let lastVisibleIndex = -1; // keep track of last index on screen + let isShowingElements = false; + + for (let i = 0; i < children.length; i++) { + const currentElementHeight = children[i].clientHeight; + + // can we fit this element in the current page? + const isNextPage = currentPageHeight + currentElementHeight > containerHeight; + if (isNextPage) { + currentPageHeight = 0; + numPages += 1; + } + + // we hide elements that are before and after the first element to show + if (i < firstIndex) { + hideElement(children[i]); + } else if (lastVisibleIndex === -1) { + isShowingElements = true; + currentPage = numPages; + } else if (isNextPage) { + isShowingElements = false; + } + + if (!isShowingElements) { + hideElement(children[i]); + } else { + lastVisibleIndex = i; + showElement(children[i], currentPageHeight); + } + + currentPageHeight += currentElementHeight; + } + + setVisiblePage(currentPage); + setNumPages(numPages); + lastIndex.current = lastVisibleIndex; + + function showElement(element: HTMLElement, yPosition: number) { + element.style.top = `${yPosition}px`; + } + + function hideElement(element: HTMLElement) { + element.style.top = `${-1000}px`; + } + // we need to add the events to make sure the effect runs on first render + }, [firstIndex, events]); + + // schedule cycling through events + useEffect(() => { + if (stopCycle) { + setVisiblePage(1); + setFirstIndex(0); + return; + } + + if (paginator.current) { + clearInterval(paginator.current); + } + + const interval = setInterval(() => { + // ensure we cycle back to the first event + if (visiblePage === numPages) { + setFirstIndex(0); + } else { + setFirstIndex(lastIndex.current + 1); + } + }, cycleInterval * 1000); + paginator.current = interval; + + return () => clearInterval(paginator.current); + }, [cycleInterval, numPages, stopCycle, visiblePage]); let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId); - const viewEvents = [...events]; - if (hidePast) { - // we want to show the event after the next - viewEvents.splice(0, selectedEventIndex + 2); - selectedEventIndex = 0; - } - - const numPages = Math.ceil(viewEvents.length / eventsPerPage); - const eventStart = eventsPerPage * visiblePage; - const eventEnd = eventsPerPage * (visiblePage + 1); - const paginatedEvents = viewEvents.slice(eventStart, eventEnd); - - const resolveScheduleType = () => { - if (selectedEventIndex >= eventStart && selectedEventIndex < eventEnd) { - return 'now'; - } - if (selectedEventIndex > eventEnd) { - return 'past'; - } - return 'future'; - }; - const scheduleType = resolveScheduleType(); - - // every SCROLL_TIME go to the next array - useInterval(() => { - if (stopCycle) { - setVisiblePage(0); - } else if (events.length > eventsPerPage) { - const next = (visiblePage + 1) % numPages; - setVisiblePage(next); - } - }, time * 1000); + // we want to show the event after the current + const viewEvents = events.toSpliced(0, selectedEventIndex + 1); + selectedEventIndex = 0; return ( {children} diff --git a/apps/client/src/views/common/schedule/ScheduleItem.tsx b/apps/client/src/views/common/schedule/ScheduleItem.tsx index f8ee147a5..b67c06a99 100644 --- a/apps/client/src/views/common/schedule/ScheduleItem.tsx +++ b/apps/client/src/views/common/schedule/ScheduleItem.tsx @@ -1,3 +1,4 @@ +import { cx } from '../../../common/utils/styleUtils'; import { formatTime } from '../../../common/utils/time'; import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime'; @@ -9,33 +10,55 @@ const formatOptions = { }; interface ScheduleItemProps { - selected: 'past' | 'now' | 'future'; timeStart: number; timeEnd: number; title: string; backstageEvent: boolean; - colour: string; - skip: boolean; + colour?: string; + skip?: boolean; + delay: number; } export default function ScheduleItem(props: ScheduleItemProps) { - const { selected, timeStart, timeEnd, title, backstageEvent, colour, skip } = props; + const { timeStart, timeEnd, title, backstageEvent, colour, skip, delay } = props; const start = formatTime(timeStart, formatOptions); const end = formatTime(timeEnd, formatOptions); - const userColour = colour !== '' ? colour : ''; - const selectStyle = `entry--${selected}`; + + if (delay > 0) { + const delayedStart = formatTime(timeStart + delay, formatOptions); + const delayedEnd = formatTime(timeEnd + delay, formatOptions); + + return ( +
    • +
      + + + + {' → '} + + {backstageEvent && '*'} + + + + {' → '} + + {backstageEvent && '*'} + +
      +
      {title}
      +
    • + ); + } return ( -
    • +
    • - -
      - - {' → '} - - {backstageEvent ? '*' : ''} -
      + + + {' → '} + + {backstageEvent && '*'}
      {title}
    • diff --git a/apps/client/src/views/common/schedule/ScheduleNav.tsx b/apps/client/src/views/common/schedule/ScheduleNav.tsx index da5516951..1602b0834 100644 --- a/apps/client/src/views/common/schedule/ScheduleNav.tsx +++ b/apps/client/src/views/common/schedule/ScheduleNav.tsx @@ -1,3 +1,5 @@ +import { cx } from '../../../common/utils/styleUtils'; + import { useSchedule } from './ScheduleContext'; import './Schedule.scss'; @@ -9,13 +11,38 @@ interface ScheduleNavProps { export default function ScheduleNav({ className }: ScheduleNavProps) { const { numPages, visiblePage } = useSchedule(); + // cap the amount of elements to 11 + if (numPages > 10) { + return ( +
      +
      +
      +
      +
      +
      +
      5 && visiblePage < numPages - 4 && 'schedule-nav__item--selected', + ])} + /> +
      +
      +
      +
      +
      +
      + ); + } + return ( -
      +
      {numPages > 1 && [...Array(numPages).keys()].map((i) => (
      ))}
      diff --git a/apps/client/src/views/common/schedule/schedule.utils.ts b/apps/client/src/views/common/schedule/schedule.utils.ts new file mode 100644 index 000000000..934cb156c --- /dev/null +++ b/apps/client/src/views/common/schedule/schedule.utils.ts @@ -0,0 +1,19 @@ +import { OntimeEvent } from 'ontime-types'; + +/** + * Gather rules for how to present scheduled times + */ +export function getScheduledTimes(event: OntimeEvent, isProduction?: boolean) { + if (isProduction) { + return { + timeStart: event.timeStart, + timeEnd: event.timeEnd, + delay: event.skip ? 0 : event.delay, + }; + } + return { + timeStart: event.timeStart, + timeEnd: event.timeEnd, + delay: 0, + }; +} diff --git a/apps/client/src/views/timer/Timer.scss b/apps/client/src/views/timer/Timer.scss index 95e9409f4..7358d4d2c 100644 --- a/apps/client/src/views/timer/Timer.scss +++ b/apps/client/src/views/timer/Timer.scss @@ -198,8 +198,8 @@ /* =================== LOGO ===================*/ .logo { position: absolute; - top: min(2vh, 16px); - left: min(2vw, 16px); + top: $view-block-padding; + left: $view-inline-padding; max-width: min(200px, 20vw); max-height: min(100px, 20vh); } diff --git a/apps/server/src/user/styles/override.css b/apps/server/src/user/styles/override.css index e1ab8bf83..5fffb6c72 100644 --- a/apps/server/src/user/styles/override.css +++ b/apps/server/src/user/styles/override.css @@ -25,6 +25,7 @@ --timer-warning-color-override: #ffbc56; --timer-danger-color-override: #e69000; --timer-overtime-color-override: #fa5656; + --timer-pending-color-override: #578AF4; /** Background for card elements on background */ --card-background-color-override: #fff;