mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 07:29:08 +00:00
feat: expand user options for views with schedule (#565)
* feat: expand user options for views with schedule * fix: selected public id on load * fix: load events on roll * style: remove empty container
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
import Empty from '../state/Empty';
|
|
||||||
|
|
||||||
import { useSchedule } from './ScheduleContext';
|
import { useSchedule } from './ScheduleContext';
|
||||||
import ScheduleItem from './ScheduleItem';
|
import ScheduleItem from './ScheduleItem';
|
||||||
|
|
||||||
@@ -13,8 +11,9 @@ interface ScheduleProps {
|
|||||||
export default function Schedule({ isProduction, className }: ScheduleProps) {
|
export default function Schedule({ isProduction, className }: ScheduleProps) {
|
||||||
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
|
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
|
||||||
|
|
||||||
|
// TODO: design a nice placeholder for empty schedules
|
||||||
if (paginatedEvents?.length < 1) {
|
if (paginatedEvents?.length < 1) {
|
||||||
return <Empty text='No events to show' />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedState: 'past' | 'now' | 'future' = 'past';
|
let selectedState: 'past' | 'now' | 'future' = 'past';
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { createContext, PropsWithChildren, useContext, useState } from 'react';
|
import { createContext, PropsWithChildren, useContext, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { OntimeEvent } from 'ontime-types';
|
import { OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { useInterval } from '../../hooks/useInterval';
|
import { useInterval } from '../../hooks/useInterval';
|
||||||
|
import { isStringBoolean } from '../../utils/viewUtils';
|
||||||
|
|
||||||
interface ScheduleContextState {
|
interface ScheduleContextState {
|
||||||
events: OntimeEvent[];
|
events: OntimeEvent[];
|
||||||
@@ -32,12 +34,25 @@ export const ScheduleProvider = ({
|
|||||||
time = 10,
|
time = 10,
|
||||||
}: PropsWithChildren<ScheduleProviderProps>) => {
|
}: PropsWithChildren<ScheduleProviderProps>) => {
|
||||||
const [visiblePage, setVisiblePage] = useState(0);
|
const [visiblePage, setVisiblePage] = useState(0);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const numPages = Math.ceil(events.length / eventsPerPage);
|
// look for overrides from views
|
||||||
|
const hidePast = isStringBoolean(searchParams.get('hidePast'));
|
||||||
|
const stopCycle = isStringBoolean(searchParams.get('stopCycle'));
|
||||||
|
|
||||||
|
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 eventStart = eventsPerPage * visiblePage;
|
||||||
const eventEnd = eventsPerPage * (visiblePage + 1);
|
const eventEnd = eventsPerPage * (visiblePage + 1);
|
||||||
const paginatedEvents = events.slice(eventStart, eventEnd);
|
const paginatedEvents = viewEvents.slice(eventStart, eventEnd);
|
||||||
const selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
|
|
||||||
|
|
||||||
const resolveScheduleType = () => {
|
const resolveScheduleType = () => {
|
||||||
if (selectedEventIndex >= eventStart && selectedEventIndex < eventEnd) {
|
if (selectedEventIndex >= eventStart && selectedEventIndex < eventEnd) {
|
||||||
@@ -52,7 +67,9 @@ export const ScheduleProvider = ({
|
|||||||
|
|
||||||
// every SCROLL_TIME go to the next array
|
// every SCROLL_TIME go to the next array
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
if (events.length > eventsPerPage) {
|
if (stopCycle) {
|
||||||
|
setVisiblePage(0);
|
||||||
|
} else if (events.length > eventsPerPage) {
|
||||||
const next = (visiblePage + 1) % numPages;
|
const next = (visiblePage + 1) % numPages;
|
||||||
setVisiblePage(next);
|
setVisiblePage(next);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,37 @@ export const LOWER_THIRDS_OPTIONS: ParamField[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const BACKSTAGE_OPTIONS: ParamField[] = [
|
||||||
|
TIME_FORMAT_OPTION,
|
||||||
|
{
|
||||||
|
id: 'hidePast',
|
||||||
|
title: 'Hide past events',
|
||||||
|
description: 'Scheduler will only show upcoming events',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stopCycle',
|
||||||
|
title: 'Stop cycling through event pages',
|
||||||
|
description: 'Schedule will not auto-cycle through events',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PUBLIC_OPTIONS: ParamField[] = [
|
||||||
|
TIME_FORMAT_OPTION,
|
||||||
|
{
|
||||||
|
id: 'hidePast',
|
||||||
|
title: 'Hide past events',
|
||||||
|
description: 'Scheduler will only show upcoming events',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stopCycle',
|
||||||
|
title: 'Stop cycling through event pages',
|
||||||
|
description: 'Schedule will not auto-cycle through events',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
];
|
||||||
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
|
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
|
||||||
TIME_FORMAT_OPTION,
|
TIME_FORMAT_OPTION,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable react/display-name */
|
/* eslint-disable react/display-name */
|
||||||
import { ComponentType, useMemo } from 'react';
|
import { ComponentType, useMemo } from 'react';
|
||||||
|
import { SupportedEvent } from 'ontime-types';
|
||||||
import { useStore } from 'zustand';
|
import { useStore } from 'zustand';
|
||||||
|
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
@@ -20,7 +21,7 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
|||||||
|
|
||||||
const publicEvents = useMemo(() => {
|
const publicEvents = useMemo(() => {
|
||||||
if (Array.isArray(rundownData)) {
|
if (Array.isArray(rundownData)) {
|
||||||
return rundownData.filter((e) => e.type === 'event' && e.title && e.isPublic);
|
return rundownData.filter((e) => e.type === SupportedEvent.Event && e.title && e.isPublic);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [rundownData]);
|
}, [rundownData]);
|
||||||
|
|||||||
@@ -99,7 +99,11 @@
|
|||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 7.5em;
|
}
|
||||||
|
|
||||||
|
.timer-gap {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 7.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aux-timers {
|
.aux-timers {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Schedule from '../../../common/components/schedule/Schedule';
|
|||||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-editor/constants';
|
import { BACKSTAGE_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
@@ -92,7 +92,7 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={[TIME_FORMAT_OPTION]} />
|
<ViewParamsEditor paramFields={BACKSTAGE_OPTIONS} />
|
||||||
<div className='project-header'>
|
<div className='project-header'>
|
||||||
{general.title}
|
{general.title}
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
@@ -130,10 +130,12 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
|
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
|
||||||
<div className='aux-timers__value'>{startedAt}</div>
|
<div className='aux-timers__value'>{startedAt}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='timer-gap' />
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
<div className='aux-timers__label'>{getLocalizedString('common.expected_finish')}</div>
|
<div className='aux-timers__label'>{getLocalizedString('common.expected_finish')}</div>
|
||||||
<div className='aux-timers__value'>{expectedFinish}</div>
|
<div className='aux-timers__value'>{expectedFinish}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='timer-gap' />
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
<div className='aux-timers__label'>{getLocalizedString('common.stage_timer')}</div>
|
<div className='aux-timers__label'>{getLocalizedString('common.stage_timer')}</div>
|
||||||
<div className='aux-timers__value'>{stageTimer}</div>
|
<div className='aux-timers__value'>{stageTimer}</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Schedule from '../../../common/components/schedule/Schedule';
|
|||||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-editor/constants';
|
import { PUBLIC_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
@@ -58,7 +58,7 @@ export default function Public(props: BackstageProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={[TIME_FORMAT_OPTION]} />
|
<ViewParamsEditor paramFields={PUBLIC_OPTIONS} />
|
||||||
<div className='project-header'>
|
<div className='project-header'>
|
||||||
{general.title}
|
{general.title}
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ export class EventLoader {
|
|||||||
this.loaded.nextEventId = nextEvent?.id || null;
|
this.loaded.nextEventId = nextEvent?.id || null;
|
||||||
this.loaded.nextPublicEventId = nextPublicEvent?.id || null;
|
this.loaded.nextPublicEventId = nextPublicEvent?.id || null;
|
||||||
|
|
||||||
|
this._loadEvent();
|
||||||
|
|
||||||
return { currentEvent, nextEvent, timeToNext };
|
return { currentEvent, nextEvent, timeToNext };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +283,7 @@ export class EventLoader {
|
|||||||
// check if current is also public
|
// check if current is also public
|
||||||
if (event.isPublic) {
|
if (event.isPublic) {
|
||||||
this.publicEventNow = event;
|
this.publicEventNow = event;
|
||||||
|
this.loaded.selectedPublicEventId = event.id;
|
||||||
} else {
|
} else {
|
||||||
// assume there is no public event
|
// assume there is no public event
|
||||||
this.publicEventNow = null;
|
this.publicEventNow = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user