From a2c832bda76464d42ec2e3b94bfad15e1db77572 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Mon, 3 Feb 2025 20:01:34 +0100 Subject: [PATCH 1/5] chore: folder structure --- apps/client/src/AppRouter.tsx | 2 +- .../src/features/viewers/public/Public.tsx | 6 ++-- .../backstage/Backstage.scss | 2 +- .../viewers => views}/backstage/Backstage.tsx | 30 +++++++++---------- .../backstage/backstage.options.ts | 4 +-- .../common}/schedule/Schedule.scss | 0 .../common}/schedule/Schedule.tsx | 0 .../common}/schedule/ScheduleContext.tsx | 2 +- .../common}/schedule/ScheduleItem.tsx | 2 +- .../common}/schedule/ScheduleNav.tsx | 0 10 files changed, 24 insertions(+), 24 deletions(-) rename apps/client/src/{features/viewers => views}/backstage/Backstage.scss (98%) rename apps/client/src/{features/viewers => views}/backstage/Backstage.tsx (83%) rename apps/client/src/{features/viewers => views}/backstage/backstage.options.ts (91%) rename apps/client/src/{common/components => views/common}/schedule/Schedule.scss (100%) rename apps/client/src/{common/components => views/common}/schedule/Schedule.tsx (100%) rename apps/client/src/{common/components => views/common}/schedule/ScheduleContext.tsx (97%) rename apps/client/src/{common/components => views/common}/schedule/ScheduleItem.tsx (95%) rename apps/client/src/{common/components => views/common}/schedule/ScheduleNav.tsx (100%) diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 3d31e63241..8287542ecf 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -27,7 +27,7 @@ const MinimalTimerView = React.lazy(() => import('./features/viewers/minimal-tim const ClockView = React.lazy(() => import('./features/viewers/clock/Clock')); const Countdown = React.lazy(() => import('./features/viewers/countdown/Countdown')); -const Backstage = React.lazy(() => import('./features/viewers/backstage/Backstage')); +const Backstage = React.lazy(() => import('./views/backstage/Backstage')); const Timeline = React.lazy(() => import('./views/timeline/TimelinePage')); const Public = React.lazy(() => import('./features/viewers/public/Public')); const Lower = React.lazy(() => import('./features/viewers/lower-thirds/LowerThird')); diff --git a/apps/client/src/features/viewers/public/Public.tsx b/apps/client/src/features/viewers/public/Public.tsx index f53ddb05af..c2c08eb2f0 100644 --- a/apps/client/src/features/viewers/public/Public.tsx +++ b/apps/client/src/features/viewers/public/Public.tsx @@ -3,9 +3,6 @@ import { useSearchParams } from 'react-router-dom'; import { AnimatePresence, motion } from 'framer-motion'; import { CustomFields, OntimeEvent, ProjectData, Settings } from 'ontime-types'; -import Schedule from '../../../common/components/schedule/Schedule'; -import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext'; -import ScheduleNav from '../../../common/components/schedule/ScheduleNav'; 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'; @@ -13,6 +10,9 @@ 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 { titleVariants } from '../common/animation'; import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; import { getPropertyValue } from '../common/viewUtils'; diff --git a/apps/client/src/features/viewers/backstage/Backstage.scss b/apps/client/src/views/backstage/Backstage.scss similarity index 98% rename from apps/client/src/features/viewers/backstage/Backstage.scss rename to apps/client/src/views/backstage/Backstage.scss index 6b80a5fda7..d359a2c2a1 100644 --- a/apps/client/src/features/viewers/backstage/Backstage.scss +++ b/apps/client/src/views/backstage/Backstage.scss @@ -1,4 +1,4 @@ -@use '../../../theme/viewerDefs' as *; +@use '../../theme/viewerDefs' as *; .backstage { margin: 0; diff --git a/apps/client/src/features/viewers/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx similarity index 83% rename from apps/client/src/features/viewers/backstage/Backstage.tsx rename to apps/client/src/views/backstage/Backstage.tsx index f429e18441..af44661f5f 100644 --- a/apps/client/src/features/viewers/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -5,21 +5,21 @@ import { AnimatePresence, motion } from 'framer-motion'; import { CustomFields, OntimeEvent, ProjectData, Settings, SupportedEvent } from 'ontime-types'; import { millisToString, removeLeadingZero } from 'ontime-utils'; -import ProgressBar from '../../../common/components/progress-bar/ProgressBar'; -import Schedule from '../../../common/components/schedule/Schedule'; -import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext'; -import ScheduleNav from '../../../common/components/schedule/ScheduleNav'; -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 { formatTime, getDefaultFormat } from '../../../common/utils/time'; -import { useTranslation } from '../../../translation/TranslationProvider'; -import { titleVariants } from '../common/animation'; -import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; -import { getPropertyValue } from '../common/viewUtils'; +import ProgressBar from '../../common/components/progress-bar/ProgressBar'; +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 { 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 { getBackstageOptions } from './backstage.options'; diff --git a/apps/client/src/features/viewers/backstage/backstage.options.ts b/apps/client/src/views/backstage/backstage.options.ts similarity index 91% rename from apps/client/src/features/viewers/backstage/backstage.options.ts rename to apps/client/src/views/backstage/backstage.options.ts index be763604b8..620bc81fed 100644 --- a/apps/client/src/features/viewers/backstage/backstage.options.ts +++ b/apps/client/src/views/backstage/backstage.options.ts @@ -4,8 +4,8 @@ import { getTimeOption, makeOptionsFromCustomFields, OptionTitle, -} from '../../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../../common/components/view-params-editor/types'; +} from '../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../common/components/view-params-editor/types'; export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' }); diff --git a/apps/client/src/common/components/schedule/Schedule.scss b/apps/client/src/views/common/schedule/Schedule.scss similarity index 100% rename from apps/client/src/common/components/schedule/Schedule.scss rename to apps/client/src/views/common/schedule/Schedule.scss diff --git a/apps/client/src/common/components/schedule/Schedule.tsx b/apps/client/src/views/common/schedule/Schedule.tsx similarity index 100% rename from apps/client/src/common/components/schedule/Schedule.tsx rename to apps/client/src/views/common/schedule/Schedule.tsx diff --git a/apps/client/src/common/components/schedule/ScheduleContext.tsx b/apps/client/src/views/common/schedule/ScheduleContext.tsx similarity index 97% rename from apps/client/src/common/components/schedule/ScheduleContext.tsx rename to apps/client/src/views/common/schedule/ScheduleContext.tsx index 087641a1b0..6c72a66214 100644 --- a/apps/client/src/common/components/schedule/ScheduleContext.tsx +++ b/apps/client/src/views/common/schedule/ScheduleContext.tsx @@ -2,8 +2,8 @@ import { createContext, PropsWithChildren, useContext, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { OntimeEvent } from 'ontime-types'; +import { useInterval } from '../../../common/hooks/useInterval'; import { isStringBoolean } from '../../../features/viewers/common/viewUtils'; -import { useInterval } from '../../hooks/useInterval'; interface ScheduleContextState { events: OntimeEvent[]; diff --git a/apps/client/src/common/components/schedule/ScheduleItem.tsx b/apps/client/src/views/common/schedule/ScheduleItem.tsx similarity index 95% rename from apps/client/src/common/components/schedule/ScheduleItem.tsx rename to apps/client/src/views/common/schedule/ScheduleItem.tsx index a1029b51f4..f8ee147a59 100644 --- a/apps/client/src/common/components/schedule/ScheduleItem.tsx +++ b/apps/client/src/views/common/schedule/ScheduleItem.tsx @@ -1,5 +1,5 @@ +import { formatTime } from '../../../common/utils/time'; import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime'; -import { formatTime } from '../../utils/time'; import './Schedule.scss'; diff --git a/apps/client/src/common/components/schedule/ScheduleNav.tsx b/apps/client/src/views/common/schedule/ScheduleNav.tsx similarity index 100% rename from apps/client/src/common/components/schedule/ScheduleNav.tsx rename to apps/client/src/views/common/schedule/ScheduleNav.tsx From 7c2b5f0e5fece8d4b637283c9b57d4d1ddd207dd Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Tue, 4 Feb 2025 22:27:54 +0100 Subject: [PATCH 2/5] refactor: backstage design review refactor: improve empty state refactor: review schedule design - fit as many elements as possible - expose cycle interval as an option - stabilise rundown reference - style tweaks --- .../src/common/components/state/Empty.tsx | 6 +- .../components/title-card/TitleCard.scss | 22 +-- .../components/title-card/TitleCard.tsx | 5 +- .../common/components/view-logo/ViewLogo.tsx | 2 +- .../src/common/hooks-query/useRundown.ts | 23 ++- apps/client/src/common/hooks/useInterval.js | 27 --- .../superscript-time/SuperscriptTime.scss | 2 +- .../src/features/viewers/public/Public.tsx | 23 +-- apps/client/src/theme/_viewerDefs.scss | 15 +- apps/client/src/translation/languages/de.ts | 1 + apps/client/src/translation/languages/en.ts | 1 + apps/client/src/translation/languages/es.ts | 1 + apps/client/src/translation/languages/fr.ts | 1 + apps/client/src/translation/languages/hu.ts | 1 + apps/client/src/translation/languages/it.ts | 1 + apps/client/src/translation/languages/no.ts | 1 + apps/client/src/translation/languages/pl.ts | 1 + apps/client/src/translation/languages/pt.ts | 1 + apps/client/src/translation/languages/sv.ts | 1 + apps/client/src/translation/languages/zh.ts | 1 + .../client/src/views/backstage/Backstage.scss | 128 ++++++++++--- apps/client/src/views/backstage/Backstage.tsx | 174 ++++++++++-------- .../src/views/backstage/backstage.options.ts | 72 ++++++-- .../src/views/backstage/backstage.utils.ts | 61 ++++++ .../common/schedule/BackstageSchedule.tsx | 21 +++ .../views/common/schedule/PublicSchedule.tsx | 21 +++ .../src/views/common/schedule/Schedule.scss | 99 +++++----- .../src/views/common/schedule/Schedule.tsx | 31 +--- .../views/common/schedule/ScheduleContext.tsx | 158 +++++++++++----- .../views/common/schedule/ScheduleItem.tsx | 51 +++-- .../src/views/common/schedule/ScheduleNav.tsx | 31 +++- .../views/common/schedule/schedule.utils.ts | 19 ++ apps/client/src/views/timer/Timer.scss | 4 +- apps/server/src/user/styles/override.css | 1 + 34 files changed, 680 insertions(+), 327 deletions(-) delete mode 100644 apps/client/src/common/hooks/useInterval.js create mode 100644 apps/client/src/views/backstage/backstage.utils.ts create mode 100644 apps/client/src/views/common/schedule/BackstageSchedule.tsx create mode 100644 apps/client/src/views/common/schedule/PublicSchedule.tsx create mode 100644 apps/client/src/views/common/schedule/schedule.utils.ts diff --git a/apps/client/src/common/components/state/Empty.tsx b/apps/client/src/common/components/state/Empty.tsx index e44146b84f..08662b0344 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 73a9de8b45..d2bfc598eb 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__placeholder { + font-weight: 600; + font-size: $title-font-size; + line-height: 1.2em; } .title-card__title { - font-weight: 600; - font-size: $title-primary-size; color: var(--color-override, $viewer-color); - line-height: 1.2em; + 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 6613f895b9..d2f773305c 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 1988d485fd..6ddde4593e 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 d123986916..67629d4d94 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 dd25a85854..0000000000 --- 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 408df72640..0ccc50f6b9 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 c2c08eb2f0..db61f8645b 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 71d37489db..a8a8a1b8b1 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 e75c293aa6..03e6837eb2 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 c0889b05bb..9dfa133476 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 e63a30c4ba..dac98ce31a 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 5ed6eab3f1..521ccda926 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 46669cbbe5..542f31888f 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 830425c4b9..41244fb7a0 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 075aa142e6..455623d7c0 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 a974c18271..6b8b30e4fa 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 fdae87c280..e58f425fd2 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 8fc9c9fb70..2187d9eab2 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 13345438ca..116f2b6004 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 d359a2c2a1..4955104bcb 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; + } + } +} + +/* =================== 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%; } } } -/* =================== AMIMATION ===================*/ +/* =================== ANIMATION ===================*/ @keyframes blink { 0% { diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index af44661f5f..eb89b66c59 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]); - 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'; + // gather card data + const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData( + eventNow, + eventNext, + 'title', + secondarySource, + time.playback, + ); - const secondarySource = searchParams.get('secondary-src'); - const secondaryTextNext = getPropertyValue(eventNext, secondarySource); - const secondaryTextNow = getPropertyValue(eventNow, secondarySource); + // gather timer data + const clock = formatTime(time.clock); + 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}
- ) : ( - - )} + {backstageEvents.length === 0 && ( +
+ +
+ )} + +
+ {showNow ? ( +
+ +
+
+
+ {isPendingStart ? getLocalizedString('countdown.waiting') : getLocalizedString('common.started_at')}
-
-
-
{getLocalizedString('common.stage_timer')}
-
{stageTimer}
+ +
+
+
+
{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')}
+
- - )} - - - - {eventNext && ( - - )} - +
+
+ )} + + {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 620bc81fed..ccf4f94e35 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 0000000000..03086b3c5d --- /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 0000000000..f29790b6bc --- /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 0000000000..b6c16d815e --- /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 c7239904c5..ce83872f81 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); - - .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; - } + position: absolute; + padding-bottom: 1em; - .entry-times { - font-family: $viewer-font-family; - color: var(--secondary-color-override, $viewer-secondary-color); - font-weight: 300; - letter-spacing: 0.05em; - display: flex; - align-items: center; - gap: 8px; + &--skip { + text-decoration: line-through; + text-decoration-color: var(--secondary-color-override, $viewer-secondary-color); } + } - .entry-title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + .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-secondary { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + .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; - &:not(:last-child) { - padding-bottom: 8px; + &--delayed, + &--delay { + display: flex; + align-items: center; + gap: 0.25rem; } - &--past { - color: var(--secondary-color-override, $viewer-secondary-color); + &--delayed { + text-decoration: line-through; + text-decoration-color: $ontime-delay; } - &--now { - .entry-title { - color: var(--accent-color-override, $accent-color); - font-weight: 600; - } + &--delay { + color: $ontime-delay-text; } + } - &.skip { - text-decoration: line-through; - } + .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 a7e394a5f7..acb6d54d88 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 6c72a66214..a690c3d76c 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 { cycleInterval, stopCycle } = useScheduleOptions(); + const { data: events } = usePartialRundown((event: OntimeRundownEntry) => { + if (isBackstage) { + return isOntimeEvent(event); + } + return isOntimeEvent(event) && event.isPublic && !event.skip; + }); + + const [firstIndex, setFirstIndex] = useState(-1); + const [numPages, setNumPages] = useState(0); const [visiblePage, setVisiblePage] = useState(0); - const [searchParams] = useSearchParams(); - // 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 lastIndex = useRef(-1); + const paginator = useRef(); - let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId); + const containerRef = useRef(null); - const viewEvents = [...events]; - if (hidePast) { - // we want to show the event after the next - viewEvents.splice(0, selectedEventIndex + 2); - selectedEventIndex = 0; - } + 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; + } - const numPages = Math.ceil(viewEvents.length / eventsPerPage); - const eventStart = eventsPerPage * visiblePage; - const eventEnd = eventsPerPage * (visiblePage + 1); - const paginatedEvents = viewEvents.slice(eventStart, eventEnd); + if (!isShowingElements) { + hideElement(children[i]); + } else { + lastVisibleIndex = i; + showElement(children[i], currentPageHeight); + } - const resolveScheduleType = () => { - if (selectedEventIndex >= eventStart && selectedEventIndex < eventEnd) { - return 'now'; + currentPageHeight += currentElementHeight; } - if (selectedEventIndex > eventEnd) { - return 'past'; + + 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`; } - return 'future'; - }; - const scheduleType = resolveScheduleType(); + // we need to add the events to make sure the effect runs on first render + }, [firstIndex, events]); - // every SCROLL_TIME go to the next array - useInterval(() => { + // schedule cycling through events + useEffect(() => { if (stopCycle) { - setVisiblePage(0); - } else if (events.length > eventsPerPage) { - const next = (visiblePage + 1) % numPages; - setVisiblePage(next); + setVisiblePage(1); + setFirstIndex(0); + return; + } + + if (paginator.current) { + clearInterval(paginator.current); } - }, time * 1000); + + 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); + + // 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 f8ee147a59..b67c06a990 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 da5516951e..1602b08341 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 0000000000..934cb156cd --- /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 95e9409f4b..7358d4d2c5 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 e1ab8bf836..5fffb6c72d 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; From fb6245a53ddf37940c375da25e8aad5662621701 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sat, 15 Feb 2025 12:14:44 +0100 Subject: [PATCH 3/5] chore: directory restructure --- apps/client/src/AppRouter.tsx | 2 +- .../viewers => views}/public/Public.scss | 2 +- .../viewers => views}/public/Public.tsx | 22 +++++++++---------- .../public/public.options.ts | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) rename apps/client/src/{features/viewers => views}/public/Public.scss (98%) rename apps/client/src/{features/viewers => views}/public/Public.tsx (79%) rename apps/client/src/{features/viewers => views}/public/public.options.ts (91%) diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 8287542ecf..6a88dfef1c 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -29,7 +29,7 @@ const Countdown = React.lazy(() => import('./features/viewers/countdown/Countdow const Backstage = React.lazy(() => import('./views/backstage/Backstage')); const Timeline = React.lazy(() => import('./views/timeline/TimelinePage')); -const Public = React.lazy(() => import('./features/viewers/public/Public')); +const Public = React.lazy(() => import('./views/public/Public')); const Lower = React.lazy(() => import('./features/viewers/lower-thirds/LowerThird')); const StudioClock = React.lazy(() => import('./features/viewers/studio/StudioClock')); const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo')); diff --git a/apps/client/src/features/viewers/public/Public.scss b/apps/client/src/views/public/Public.scss similarity index 98% rename from apps/client/src/features/viewers/public/Public.scss rename to apps/client/src/views/public/Public.scss index 04b5ddc5c1..e4c5776770 100644 --- a/apps/client/src/features/viewers/public/Public.scss +++ b/apps/client/src/views/public/Public.scss @@ -1,4 +1,4 @@ -@use '../../../theme/viewerDefs' as *; +@use '../../theme/viewerDefs' as *; .public-screen { margin: 0; diff --git a/apps/client/src/features/viewers/public/Public.tsx b/apps/client/src/views/public/Public.tsx similarity index 79% rename from apps/client/src/features/viewers/public/Public.tsx rename to apps/client/src/views/public/Public.tsx index db61f8645b..3f174100a9 100644 --- a/apps/client/src/features/viewers/public/Public.tsx +++ b/apps/client/src/views/public/Public.tsx @@ -3,17 +3,17 @@ import { useSearchParams } from 'react-router-dom'; import { AnimatePresence, motion } from 'framer-motion'; import { CustomFields, OntimeEvent, ProjectData, Settings } from 'ontime-types'; -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 { formatTime, getDefaultFormat } from '../../../common/utils/time'; -import { useTranslation } from '../../../translation/TranslationProvider'; -import PublicSchedule from '../../../views/common/schedule/PublicSchedule'; -import { titleVariants } from '../common/animation'; -import SuperscriptTime from '../common/superscript-time/SuperscriptTime'; -import { getPropertyValue } from '../common/viewUtils'; +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 { 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 PublicSchedule from '../common/schedule/PublicSchedule'; +import { titleVariants } from '../timer/timer.animations'; import { getPublicOptions } from './public.options'; diff --git a/apps/client/src/features/viewers/public/public.options.ts b/apps/client/src/views/public/public.options.ts similarity index 91% rename from apps/client/src/features/viewers/public/public.options.ts rename to apps/client/src/views/public/public.options.ts index 8173907629..210abf12e9 100644 --- a/apps/client/src/features/viewers/public/public.options.ts +++ b/apps/client/src/views/public/public.options.ts @@ -4,8 +4,8 @@ import { getTimeOption, makeOptionsFromCustomFields, OptionTitle, -} from '../../../common/components/view-params-editor/constants'; -import { ViewOption } from '../../../common/components/view-params-editor/types'; +} from '../../common/components/view-params-editor/constants'; +import { ViewOption } from '../../common/components/view-params-editor/types'; export const getPublicOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => { const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' }); From f63c476751130abc1e1f3f3846a7f1d98ad88cd7 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sat, 15 Feb 2025 12:59:06 +0100 Subject: [PATCH 4/5] refactor: public design review --- .../components/progress-bar/ProgressBar.scss | 4 - .../components/progress-bar/ProgressBar.tsx | 5 +- .../common/components/state/Empty.module.scss | 2 +- .../common/components/view-logo/ViewLogo.scss | 5 + .../common/components/view-logo/ViewLogo.tsx | 10 +- .../client/src/views/backstage/Backstage.scss | 32 ++-- apps/client/src/views/backstage/Backstage.tsx | 74 +++++---- .../src/views/backstage/backstage.options.ts | 44 +----- .../src/views/backstage/backstage.utils.ts | 9 +- .../views/common/schedule/PublicSchedule.tsx | 21 --- .../src/views/common/schedule/Schedule.scss | 20 +-- .../views/common/schedule/ScheduleContext.tsx | 4 +- ...ckstageSchedule.tsx => ScheduleExport.tsx} | 11 +- .../views/common/schedule/ScheduleItem.tsx | 6 +- .../views/common/schedule/schedule.options.ts | 48 ++++++ apps/client/src/views/public/Public.scss | 141 +++++++++++++++--- apps/client/src/views/public/Public.tsx | 117 ++++++++------- .../client/src/views/public/public.options.ts | 57 +++---- apps/client/src/views/public/public.utils.ts | 45 ++++++ e2e/tests/features/203-delay-block.spec.ts | 4 +- 20 files changed, 410 insertions(+), 249 deletions(-) create mode 100644 apps/client/src/common/components/view-logo/ViewLogo.scss delete mode 100644 apps/client/src/views/common/schedule/PublicSchedule.tsx rename apps/client/src/views/common/schedule/{BackstageSchedule.tsx => ScheduleExport.tsx} (59%) create mode 100644 apps/client/src/views/common/schedule/schedule.options.ts create mode 100644 apps/client/src/views/public/public.utils.ts diff --git a/apps/client/src/common/components/progress-bar/ProgressBar.scss b/apps/client/src/common/components/progress-bar/ProgressBar.scss index 73383cc25f..21d502eab7 100644 --- a/apps/client/src/common/components/progress-bar/ProgressBar.scss +++ b/apps/client/src/common/components/progress-bar/ProgressBar.scss @@ -9,10 +9,6 @@ $progress-bar-br: 3px; border-radius: $progress-bar-br; background-color: var(--timer-progress-bg-override, $viewer-card-bg-color); overflow: clip; - - &--hidden { - display: none; - } } .progress-bar__indicator { diff --git a/apps/client/src/common/components/progress-bar/ProgressBar.tsx b/apps/client/src/common/components/progress-bar/ProgressBar.tsx index 8098a0e651..720704260a 100644 --- a/apps/client/src/common/components/progress-bar/ProgressBar.tsx +++ b/apps/client/src/common/components/progress-bar/ProgressBar.tsx @@ -7,16 +7,15 @@ import './ProgressBar.scss'; interface ProgressBarProps { current: MaybeNumber; duration: MaybeNumber; - hidden?: boolean; className?: string; } export default function ProgressBar(props: ProgressBarProps) { - const { current, duration, hidden, className = '' } = props; + const { current, duration, className } = props; const progress = getProgress(current, duration); return ( -
      +
      ); diff --git a/apps/client/src/common/components/state/Empty.module.scss b/apps/client/src/common/components/state/Empty.module.scss index 193862ebc6..9f0e9436f8 100644 --- a/apps/client/src/common/components/state/Empty.module.scss +++ b/apps/client/src/common/components/state/Empty.module.scss @@ -5,7 +5,7 @@ .empty { width: 100%; - opacity: 0.6; + opacity: 0.8; } .text { diff --git a/apps/client/src/common/components/view-logo/ViewLogo.scss b/apps/client/src/common/components/view-logo/ViewLogo.scss new file mode 100644 index 0000000000..f2b2491a9b --- /dev/null +++ b/apps/client/src/common/components/view-logo/ViewLogo.scss @@ -0,0 +1,5 @@ +.viewLogo { + max-width: 100%; + max-height: 100%; + display: block; +} diff --git a/apps/client/src/common/components/view-logo/ViewLogo.tsx b/apps/client/src/common/components/view-logo/ViewLogo.tsx index 6ddde4593e..6ed80ae1f1 100644 --- a/apps/client/src/common/components/view-logo/ViewLogo.tsx +++ b/apps/client/src/common/components/view-logo/ViewLogo.tsx @@ -1,5 +1,7 @@ import { projectLogoPath } from '../../api/constants'; +import './ViewLogo.scss'; + interface ViewLogoProps { name: string; className: string; @@ -7,5 +9,11 @@ interface ViewLogoProps { export default function ViewLogo(props: ViewLogoProps) { const { name, className } = props; - return ; + + // we wrap the image in a div to help maintain the aspect ratio + return ( +
      + +
      + ); } diff --git a/apps/client/src/views/backstage/Backstage.scss b/apps/client/src/views/backstage/Backstage.scss index 4955104bcb..f914a84fd6 100644 --- a/apps/client/src/views/backstage/Backstage.scss +++ b/apps/client/src/views/backstage/Backstage.scss @@ -12,6 +12,7 @@ color: var(--color-override, $viewer-color); gap: $view-element-gap; padding: $view-outer-padding; + font-size: $base-font-size; display: grid; grid-template-columns: 1fr 40vw; @@ -28,6 +29,7 @@ left: 0; right: 0; margin-top: 25vh; + color: var(--label-color-override, $viewer-label-color); } /* =================== HEADER + EXTRAS ===================*/ @@ -82,16 +84,6 @@ 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: $view-card-padding; @@ -106,7 +98,6 @@ } .timer-group { - grid-area: timer; border-top: 2px solid var(--background-color-override, $viewer-background-color); margin-top: max(1vh, 16px); padding-top: max(1vh, 16px); @@ -119,7 +110,7 @@ max-width: 7.5em; } - .aux-timers { + .time-entry { &__label { font-size: $timer-label-size; color: var(--label-color-override, $viewer-label-color); @@ -207,14 +198,27 @@ right: 0; } + .schedule-nav-container { + padding-left: 0; + justify-content: center; + } + .schedule-container { - margin-left: 0; + padding-left: 0; flex: 1; } .info { - flex: 1; width: 100%; + text-align: right; + } + + .qr { + display: none; + } + + .info--stretch { + flex: 1; } } } diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index eb89b66c59..6795a5db14 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -15,7 +15,7 @@ import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils'; import { formatTime, getDefaultFormat } from '../../common/utils/time'; import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime'; import { useTranslation } from '../../translation/TranslationProvider'; -import BackstageSchedule from '../common/schedule/BackstageSchedule'; +import ScheduleExport from '../common/schedule/ScheduleExport'; import { getBackstageOptions, useBackstageOptions } from './backstage.options'; import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from './backstage.utils'; @@ -68,6 +68,7 @@ export default function Backstage(props: BackstageProps) { }, [selectedId]); // gather card data + const hasEvents = backstageEvents.length > 0; const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData( eventNow, eventNext, @@ -80,16 +81,18 @@ export default function Backstage(props: BackstageProps) { const clock = formatTime(time.clock); 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' }); + const scheduledStart = + hasEvents && showNow ? '' : formatTime(runtime.plannedStart, { format12: 'hh:mm a', format24: 'HH:mm' }); + const scheduledEnd = + hasEvents && showNow ? '' : formatTime(runtime.plannedEnd, { format12: 'hh:mm a', format24: 'HH:mm' }); - let stageTimer = millisToString(time.current, { fallback: timerPlaceholderMin }); - stageTimer = removeLeadingZero(stageTimer); + let displayTimer = millisToString(time.current, { fallback: timerPlaceholderMin }); + displayTimer = removeLeadingZero(displayTimer); // 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 + const showSchedule = hasEvents && screenHeight > 700; // in vertical screens we may not have space // gather option data const defaultFormat = getDefaultFormat(settings?.timeFormat); @@ -107,71 +110,66 @@ export default function Backstage(props: BackstageProps) {
      -