diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 3d31e63241..6a88dfef1c 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -27,9 +27,9 @@ 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 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/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/schedule/Schedule.scss b/apps/client/src/common/components/schedule/Schedule.scss deleted file mode 100644 index c7239904c5..0000000000 --- a/apps/client/src/common/components/schedule/Schedule.scss +++ /dev/null @@ -1,76 +0,0 @@ -@use '../../../theme/viewerDefs' as *; - -.schedule { - width: 100%; - border-spacing: 50px; - - .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; - } - - .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; - } - - .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 { - text-decoration: line-through; - } - } -} - -.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; - border-radius: 6px; - margin-left: 8px; - - &--selected { - background-color: var(--color-override, $viewer-color); - } - } -} diff --git a/apps/client/src/common/components/schedule/Schedule.tsx b/apps/client/src/common/components/schedule/Schedule.tsx deleted file mode 100644 index a7e394a5f7..0000000000 --- a/apps/client/src/common/components/schedule/Schedule.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useSchedule } from './ScheduleContext'; -import ScheduleItem from './ScheduleItem'; - -import './Schedule.scss'; - -interface ScheduleProps { - isProduction?: boolean; - className?: string; -} - -export default function Schedule({ isProduction, className }: ScheduleProps) { - const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule(); - - // TODO: design a nice placeholder for empty schedules - if (paginatedEvents?.length < 1) { - return null; - } - - let selectedState: 'past' | 'now' | 'future' = 'past'; - - return ( - - ); -} diff --git a/apps/client/src/common/components/schedule/ScheduleContext.tsx b/apps/client/src/common/components/schedule/ScheduleContext.tsx deleted file mode 100644 index 087641a1b0..0000000000 --- a/apps/client/src/common/components/schedule/ScheduleContext.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { createContext, PropsWithChildren, useContext, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { OntimeEvent } from 'ontime-types'; - -import { isStringBoolean } from '../../../features/viewers/common/viewUtils'; -import { useInterval } from '../../hooks/useInterval'; - -interface ScheduleContextState { - events: OntimeEvent[]; - paginatedEvents: OntimeEvent[]; - selectedEventId: string | null; - scheduleType: 'past' | 'now' | 'future'; - numPages: number; - visiblePage: number; - isBackstage: boolean; -} - -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(); - - // look for overrides from views - const hidePast = isStringBoolean(searchParams.get('hidePast')); - const stopCycle = isStringBoolean(searchParams.get('stopCycle')); - const eventsPerPage = Number(searchParams.get('eventsPerPage') ?? numEventsPerPage); - - 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); - - return ( - - {children} - - ); -}; - -export const useSchedule = () => { - const context = useContext(ScheduleContext); - if (!context) { - throw new Error('useSchedule() can only be used inside a ScheduleContext'); - } - return context; -}; diff --git a/apps/client/src/common/components/schedule/ScheduleItem.tsx b/apps/client/src/common/components/schedule/ScheduleItem.tsx deleted file mode 100644 index a1029b51f4..0000000000 --- a/apps/client/src/common/components/schedule/ScheduleItem.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime'; -import { formatTime } from '../../utils/time'; - -import './Schedule.scss'; - -const formatOptions = { - format12: 'hh:mm a', - format24: 'HH:mm', -}; - -interface ScheduleItemProps { - selected: 'past' | 'now' | 'future'; - timeStart: number; - timeEnd: number; - title: string; - backstageEvent: boolean; - colour: string; - skip: boolean; -} - -export default function ScheduleItem(props: ScheduleItemProps) { - const { selected, timeStart, timeEnd, title, backstageEvent, colour, skip } = props; - - const start = formatTime(timeStart, formatOptions); - const end = formatTime(timeEnd, formatOptions); - const userColour = colour !== '' ? colour : ''; - const selectStyle = `entry--${selected}`; - - return ( -
  • -
    - -
    - - {' → '} - - {backstageEvent ? '*' : ''} -
    -
    -
    {title}
    -
  • - ); -} diff --git a/apps/client/src/common/components/schedule/ScheduleNav.tsx b/apps/client/src/common/components/schedule/ScheduleNav.tsx deleted file mode 100644 index da5516951e..0000000000 --- a/apps/client/src/common/components/schedule/ScheduleNav.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useSchedule } from './ScheduleContext'; - -import './Schedule.scss'; - -interface ScheduleNavProps { - className?: string; -} - -export default function ScheduleNav({ className }: ScheduleNavProps) { - const { numPages, visiblePage } = useSchedule(); - - return ( -
    - {numPages > 1 && - [...Array(numPages).keys()].map((i) => ( -
    - ))} -
    - ); -} 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/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.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 1988d485fd..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/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/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 6b8c7e19cb..741e9f9efb 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -170,6 +170,10 @@ export const useTimelineStatus = createSelector((state: RuntimeStore) => ({ offset: state.runtime.offset, })); +export const useRuntimeOffset = createSelector((state: RuntimeStore) => ({ + offset: state.runtime.offset, +})); + export const usePing = createSelector((state: RuntimeStore) => ({ ping: state.ping, })); diff --git a/apps/client/src/features/viewers/backstage/Backstage.tsx b/apps/client/src/features/viewers/backstage/Backstage.tsx deleted file mode 100644 index f429e18441..0000000000 --- a/apps/client/src/features/viewers/backstage/Backstage.tsx +++ /dev/null @@ -1,164 +0,0 @@ -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 { 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 { getBackstageOptions } from './backstage.options'; - -import './Backstage.scss'; - -export const MotionTitleCard = motion(TitleCard); - -interface BackstageProps { - backstageEvents: OntimeEvent[]; - customFields: CustomFields; - eventNext: OntimeEvent | null; - eventNow: OntimeEvent | null; - general: ProjectData; - isMirrored: boolean; - time: ViewExtendedTimer; - selectedId: string | null; - settings: Settings | undefined; -} - -export default function Backstage(props: BackstageProps) { - const { backstageEvents, customFields, eventNext, eventNow, general, time, isMirrored, selectedId, settings } = props; - - const { getLocalizedString } = useTranslation(); - const [blinkClass, setBlinkClass] = useState(false); - const [searchParams] = useSearchParams(); - - useWindowTitle('Backstage'); - - // blink on change - useEffect(() => { - setBlinkClass(false); - - const timer = setTimeout(() => { - setBlinkClass(true); - }, 10); - - 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'; - - const secondarySource = searchParams.get('secondary-src'); - const secondaryTextNext = getPropertyValue(eventNext, secondarySource); - const secondaryTextNow = getPropertyValue(eventNow, secondarySource); - - let stageTimer = millisToString(time.current, { fallback: timerPlaceholderMin }); - stageTimer = removeLeadingZero(stageTimer); - - const defaultFormat = getDefaultFormat(settings?.timeFormat); - const backstageOptions = getBackstageOptions(defaultFormat, customFields); - - return ( -
    - -
    - {general?.projectLogo && } - {general.title} -
    -
    {getLocalizedString('common.time_now')}
    - -
    -
    - -