v2 beta 5 (#375)

* chore: update documentation links

* fix: sentry has no access to error context

* style: clarify event history

* fix: issue with clipboard write in safari

* style: clarify event history

* refactor: improvements to follow logic in rundown

* refactor: improvements in go mode

* several style tweaks and small improvements
This commit is contained in:
Carlos Valente
2023-05-12 22:31:26 +02:00
committed by GitHub
parent 139b667e20
commit 305d6b6476
19 changed files with 122 additions and 92 deletions
@@ -14,21 +14,18 @@ interface CopyTagProps {
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
const { label, className, size = 'xs', children } = props;
const handleClick = () => {
// we need to this as a promise because safari
setTimeout(async () => await navigator.clipboard.writeText(children as string));
};
return (
<Tooltip label={label} openDelay={tooltipDelayFast}>
<ButtonGroup
size={size}
isAttached
className={className}
>
<Button variant='ontime-subtle' tabIndex={-1}>{children}</Button>
<IconButton
aria-label={label}
icon={<IoCopy />}
variant='ontime-filled'
tabIndex={-1}
onClick={() => navigator.clipboard.writeText(children as string)}
/>
<ButtonGroup size={size} isAttached className={className}>
<Button variant='ontime-subtle' tabIndex={-1}>
{children}
</Button>
<IconButton aria-label={label} icon={<IoCopy />} variant='ontime-filled' tabIndex={-1} onClick={handleClick} />
</ButtonGroup>
</Tooltip>
);
@@ -28,12 +28,6 @@ class ErrorBoundary extends React.Component {
const eventId = Sentry.captureException(error);
this.setState({ eventId, info });
});
try {
this.context.emitError(error.toString());
} catch (e) {
Sentry.captureMessage(`Unable to emit error ${error} ${e}`);
}
this.reportContent = `${error} ${info.componentStack}`;
}
+12 -1
View File
@@ -5,6 +5,16 @@ export enum AppMode {
Edit = 'edit',
}
const appModeKey = 'ontime-app-mode';
function getModeFromSession() {
return localStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
}
async function persistModeToSession(mode: AppMode) {
localStorage.setItem(appModeKey, mode);
}
type AppModeStore = {
mode: AppMode;
cursor: string | null;
@@ -15,11 +25,12 @@ type AppModeStore = {
};
export const useAppMode = create<AppModeStore>()((set) => ({
mode: AppMode.Edit,
mode: getModeFromSession(),
cursor: null,
editId: null,
setMode: (mode: AppMode) =>
set((state) => {
persistModeToSession(mode);
return mode === AppMode.Edit
? {
editId: state.cursor,
+1 -1
View File
@@ -1,4 +1,4 @@
export const githubUrl = 'https://www.github.com/cpvalente/ontime';
export const apiRepoLatest = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
export const gitbookUrl = 'https://cpvalente.gitbook.io';
export const gitbookUrl = 'https://ontime.gitbook.io';
@@ -34,17 +34,26 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
const isLast = selectedEventIndex === numEvents - 1;
const noEvents = numEvents === 0;
const disableGo = isRolling || noEvents || isLast;
const disableGo = isRolling || noEvents || (isLast && !isArmed);
const disablePrev = isRolling || noEvents || isFirst;
const goModeText = selectedEventIndex === null || isArmed ? 'Start' : 'Next';
const goModeAction = () => {
if (isArmed) {
setPlayback.start();
} else {
setPlayback.startNext();
}
};
return (
<div className={styles.buttonContainer}>
<TapButton disabled={disableGo} onClick={() => setPlayback.startNext()} aspect='fill' className={styles.go}>
GO
<TapButton disabled={disableGo} onClick={goModeAction} aspect='fill' className={styles.go}>
{goModeText}
</TapButton>
<div className={style.playbackContainer}>
<TapButton
onClick={() => setPlayback.start()}
onClick={setPlayback.start}
disabled={isStopped || isRolling}
theme={Playback.Play}
active={isPlaying}
@@ -53,7 +62,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
</TapButton>
<TapButton
onClick={() => setPlayback.pause()}
onClick={setPlayback.pause}
disabled={isStopped || isRolling || isArmed}
theme={Playback.Pause}
active={isPaused}
@@ -63,19 +72,19 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
</div>
<div className={style.transportContainer}>
<Tooltip label='Previous event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.previous()} disabled={disablePrev}>
<TapButton onClick={setPlayback.previous} disabled={disablePrev}>
<IoPlaySkipBack />
</TapButton>
</Tooltip>
<Tooltip label='Next event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.next()} disabled={disableGo}>
<TapButton onClick={setPlayback.next} disabled={disableGo}>
<IoPlaySkipForward />
</TapButton>
</Tooltip>
</div>
<div className={styles.extra}>
<TapButton
onClick={() => setPlayback.roll()}
onClick={setPlayback.roll}
disabled={!isStopped || noEvents}
theme={Playback.Roll}
active={isRolling}
@@ -83,12 +92,12 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
<IoTimeOutline />
</TapButton>
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.reload()} disabled={isStopped || isRolling}>
<TapButton onClick={setPlayback.reload} disabled={isStopped || isRolling}>
<IoReload className={style.invertX} />
</TapButton>
</Tooltip>
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
<TapButton onClick={() => setPlayback.stop()} disabled={isStopped && !isRolling} theme={Playback.Stop}>
<TapButton onClick={setPlayback.stop} disabled={isStopped && !isRolling} theme={Playback.Stop}>
<IoStop />
</TapButton>
</Tooltip>
@@ -1,9 +1,9 @@
import { memo } from 'react';
import { Box, IconButton } from '@chakra-ui/react';
import { FiX } from '@react-icons/all-files/fi/FiX';
import { IoClose } from '@react-icons/all-files/io5/IoClose';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
import { useAppMode } from '../../common/stores/appModeStore';
import { cx } from '../../common/utils/styleUtils';
import EventEditor from './EventEditor';
@@ -19,13 +19,11 @@ const closeBtnStyle = {
};
const EventEditorExport = () => {
const appMode = useAppMode((state) => state.mode);
const editId = useAppMode((state) => state.editId);
const setEditId = useAppMode((state) => state.setEditId);
const editorStyle = cx([style.eventEditor, !editId ? style.noEvent : null]);
const removeOpenEvent = () => setEditId(null);
const canRemoveOpenId = appMode === AppMode.Run;
return (
<Box className={editorStyle}>
@@ -33,13 +31,7 @@ const EventEditorExport = () => {
<div className={style.eventEditorLayout}>
<EventEditor />
<div className={style.header}>
<IconButton
aria-label='Close Menu'
icon={<FiX />}
onClick={removeOpenEvent}
isDisabled={!canRemoveOpenId}
{...closeBtnStyle}
/>
<IconButton aria-label='Close Menu' icon={<IoClose />} onClick={removeOpenEvent} {...closeBtnStyle} />
</div>
</div>
</ErrorBoundary>
@@ -1,6 +1,7 @@
import { ReactNode } from 'react';
import { MouseEvent, ReactNode } from 'react';
import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
import { openLink } from '../../common/utils/linkUtils';
import { cx } from '../../common/utils/styleUtils';
import style from './ModalLink.module.scss';
@@ -14,8 +15,14 @@ interface ModalLinkProps {
export default function ModalLink(props: ModalLinkProps) {
const { href, inline, children } = props;
const classes = cx([style.link, inline ? style.inline : null]);
const handleClick = (event: MouseEvent) => {
event.preventDefault();
openLink(href);
};
return (
<a href={href} target='_blank' rel='noreferrer' className={classes}>
<a href='#!' target='_blank' rel='noreferrer' className={classes} onClick={handleClick}>
{children} <IoOpenOutline />
</a>
);
@@ -43,7 +43,7 @@ export default function AboutModal(props: AboutModalProps) {
<div className={styles.padBottom}>
<span className={styles.sectionTitle}>Ontime</span>
Free Open Source Software for managing rundowns and event timers
<ModalLink href='www.getontime.no'>www.getontime.no</ModalLink>
<ModalLink href='https://www.getontime.no'>www.getontime.no</ModalLink>
</div>
<div className={styles.padBottom}>
<span className={styles.sectionTitle}>Current version</span>
@@ -12,7 +12,7 @@ interface IntegrationModalProps {
onClose: () => void;
}
const oscDocsUrl = 'https://cpvalente.gitbook.io/ontime/control-and-feedback/osc';
const oscDocsUrl = 'https://ontime.gitbook.io/v2/control-and-feedback/osc';
export default function IntegrationModal(props: IntegrationModalProps) {
const { isOpen, onClose } = props;
+27 -15
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types';
@@ -37,7 +37,8 @@ export default function Rundown(props: RundownProps) {
const appMode = useAppMode((state) => state.mode);
const viewFollowsCursor = appMode === AppMode.Run;
const moveCursorTo = useAppMode((state) => state.setCursor);
const cursorRef = useRef<HTMLDivElement>();
const cursorRef = useRef<HTMLDivElement | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
// DND KIT
const sensors = useSensors(useSensor(PointerSensor));
@@ -153,19 +154,25 @@ export default function Rundown(props: RundownProps) {
// when cursor moves, view should follow
useEffect(() => {
if (!cursorRef?.current) return;
function scrollToComponent(
componentRef: MutableRefObject<HTMLDivElement>,
scrollRef: MutableRefObject<HTMLDivElement>,
) {
const componentRect = componentRef.current.getBoundingClientRect();
const scrollRect = scrollRef.current.getBoundingClientRect();
const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - 100;
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
}
// using start in block parameter causes jumpy behaviour
// could alternatively scroll using scrollTo and
// calculate position within a range
// if the item is near the top half, we are ok
// otherwise scroll difference
cursorRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'start',
});
}, [cursorRef]);
if (cursorRef.current && scrollRef.current) {
// Use requestAnimationFrame to ensure the component is fully loaded
window.requestAnimationFrame(() => {
scrollToComponent(cursorRef as MutableRefObject<HTMLDivElement>, scrollRef as MutableRefObject<HTMLDivElement>);
});
}
// eslint-disable-next-line -- the prompt seems incorrect
}, [cursorRef?.current, scrollRef]);
useEffect(() => {
// in run mode, we follow selection
@@ -200,9 +207,10 @@ export default function Rundown(props: RundownProps) {
let thisEnd = 0;
let previousEventId: string | undefined;
let eventIndex = -1;
let isPast = Boolean(featureData?.selectedEventId);
return (
<div className={style.eventContainer}>
<div className={style.eventContainer} ref={scrollRef}>
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
<SortableContext items={statefulEntries} strategy={verticalListSortingStrategy}>
<div className={style.list}>
@@ -225,12 +233,16 @@ export default function Rundown(props: RundownProps) {
const isSelected = featureData?.selectedEventId === entry.id;
const isNext = featureData?.nextEventId === entry.id;
const hasCursor = entry.id === cursor;
if (isSelected) {
isPast = false;
}
return (
<div key={entry.id} ref={hasCursor ? cursorRef : undefined}>
<RundownEntry
type={entry.type}
eventIndex={eventIndex}
isPast={isPast}
data={entry}
selected={isSelected}
hasCursor={hasCursor}
@@ -17,6 +17,7 @@ export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'del
interface RundownEntryProps {
type: SupportedEvent;
eventIndex: number;
isPast: boolean;
data: OntimeRundownEntry;
selected: boolean;
hasCursor: boolean;
@@ -32,6 +33,7 @@ interface RundownEntryProps {
export default function RundownEntry(props: RundownEntryProps) {
const {
eventIndex,
isPast,
data,
selected,
hasCursor,
@@ -168,6 +170,7 @@ export default function RundownEntry(props: RundownEntryProps) {
delay={delay}
previousEnd={previousEnd}
colour={data.colour}
isPast={isPast}
next={next}
skip={data.skip}
selected={selected}
@@ -10,9 +10,9 @@ $skip-opacity: 0.1;
display: grid;
grid-template-areas:
"binder ... ... ..."
"binder pb-actions times actions"
"binder pb-actions title title"
"binder ... ... ..."
"binder pb-actions times actions"
"binder pb-actions title next"
"binder pb-actions estatus estatus"
"binder ... ... ...";
@@ -25,7 +25,7 @@ $skip-opacity: 0.1;
transition-property: background-color;
transition-duration: $transition-time-feedback;
@mixin declare-overrides(){
@mixin declare-overrides() {
--status-color-override: #{$gray-200};
--status-color-active-override: #{$green-400};
}
@@ -45,7 +45,7 @@ $skip-opacity: 0.1;
}
&.pause {
background-color: $orange-700;
background-color: rgba($ontime-paused, 0.6);
@include declare-overrides;
}
@@ -53,6 +53,10 @@ $skip-opacity: 0.1;
outline: 1px solid $block-cursor-color;
}
&.past:not(.skip) {
opacity: 0.6;
}
&.skip {
border: 1px solid $white-3;
@@ -121,7 +125,6 @@ $skip-opacity: 0.1;
justify-content: flex-end;
}
.eventOptions {
margin: $element-spacing 16px $element-spacing 0;
}
@@ -163,6 +166,16 @@ $skip-opacity: 0.1;
line-height: 13px;
}
.nextTag {
grid-area: next;
font-size: 1em;
color: $orange-500;
letter-spacing: 0.03px;
font-weight: 600;
text-align: right;
}
.eventStatus {
grid-area: status;
display: flex;
@@ -171,11 +184,6 @@ $skip-opacity: 0.1;
gap: 8px;
color: var(--status-color-override, $gray-500);
.tag {
padding-top: 1px;
font-size: 0.55em;
color: $active-indicator;
}
.statusIcon {
width: 16px;
@@ -26,6 +26,7 @@ interface EventBlockProps {
delay: number;
previousEnd: number;
colour: string;
isPast: boolean;
next: boolean;
skip: boolean;
selected: boolean;
@@ -59,6 +60,7 @@ export default function EventBlock(props: EventBlockProps) {
delay,
previousEnd,
colour,
isPast,
next,
skip = false,
selected,
@@ -128,6 +130,7 @@ export default function EventBlock(props: EventBlockProps) {
const blockClasses = cx([
style.eventBlock,
skip ? style.skip : null,
isPast ? style.past : null,
selected ? style.selected : null,
playback ? style[playback] : null,
hasCursor ? style.hasCursor : null,
@@ -113,6 +113,11 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
previousEnd={previousEnd}
/>
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
{next && (
<Tooltip label='Next event' {...tooltipProps}>
<span className={style.nextTag}>UP NEXT</span>
</Tooltip>
)}
<EventBlockPlayback
eventId={eventId}
skip={skip}
@@ -127,11 +132,6 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
{selected && <EventBlockProgressBar playback={playback} />}
</div>
<div className={style.eventStatus} tabIndex={-1}>
{next && (
<Tooltip label='Next event' {...tooltipProps}>
<span className={style.tag}>NEXT</span>
</Tooltip>
)}
<Tooltip label={`Time type: ${timerType}`} {...tooltipProps}>
<span>
<TimerIcon type={timerType} className={style.statusIcon} />
@@ -1,5 +1,5 @@
import { ComponentType, useMemo } from 'react';
import { Playback, TitleBlock } from 'ontime-types';
import { TitleBlock } from 'ontime-types';
import { useStore } from 'zustand';
import useEventData from '../../common/hooks-query/useEventData';
@@ -80,7 +80,6 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
// get clock string
const TimeManagerType = {
...timer,
finished: playback === Playback.Play && (timer.current ?? 0) < 0 && timer.startedAt,
playback,
};
@@ -91,7 +91,8 @@ export default function Countdown(props: CountdownProps) {
}
const standby = time.playback !== Playback.Play && time.playback !== Playback.Roll && selectedId === follow?.id;
const isRunningFinished = time.finished && runningMessage === TimerMessage.running;
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
const isRunningFinished = finished && runningMessage === TimerMessage.running;
const isSelected = runningMessage === TimerMessage.running;
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
@@ -130,8 +130,8 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const showEndMessage = (time.current ?? 0) < 0 && viewSettings.endMessage && !hideEndMessage;
const showFinished =
time.finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
const showFinished = finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
const showProgress = time.playback !== Playback.Stop;
const showWarning = (time.current ?? 1) < viewSettings.warningThreshold;
@@ -65,9 +65,10 @@ export default function Timer(props: TimerProps) {
const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
const showEndMessage = (time.current ?? 1) < 0 && viewSettings.endMessage;
const showProgress = time.playback !== Playback.Stop;
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
const showFinished = finished && (time.timerType !== TimerType.Clock || showEndMessage);
const showWarning = (time.current ?? 1) < viewSettings.warningThreshold;
const showDanger = (time.current ?? 1) < viewSettings.dangerThreshold;
const timerColor =
@@ -130,7 +131,7 @@ export default function Timer(props: TimerProps) {
/>
<AnimatePresence>
{title.showNow && !time.finished && (
{title.showNow && !finished && (
<motion.div
className='event now'
key='now'
+1 -8
View File
@@ -96,13 +96,6 @@ function getApplicationMenu(isMac, askToQuit) {
await shell.openExternal('http://localhost:4001/lower');
},
},
{
label: 'PiP',
click: async () => {
await shell.openExternal('http://localhost:4001/pip');
},
},
{
label: 'Studio Clock',
click: async () => {
@@ -160,7 +153,7 @@ function getApplicationMenu(isMac, askToQuit) {
{
label: 'Online documentation',
click: async () => {
await shell.openExternal('https://cpvalente.gitbook.io/ontime/');
await shell.openExternal('https://ontime.gitbook.io/');
},
},
],