From 305d6b6476b3581580a6b5befcc85732152c09de Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Fri, 12 May 2023 22:31:26 +0200 Subject: [PATCH] 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 --- .../common/components/copy-tag/CopyTag.tsx | 23 +++++----- .../error-boundary/ErrorBoundary.jsx | 6 --- apps/client/src/common/stores/appModeStore.ts | 13 +++++- apps/client/src/externals.ts | 2 +- .../playback-buttons/PlaybackButtons.tsx | 29 ++++++++----- .../event-editor/EventEditorExport.tsx | 14 ++----- apps/client/src/features/modals/ModalLink.tsx | 11 ++++- .../modals/about-modal/AboutModal.tsx | 2 +- .../integration-modal/IntegrationModal.tsx | 2 +- apps/client/src/features/rundown/Rundown.tsx | 42 ++++++++++++------- .../src/features/rundown/RundownEntry.tsx | 3 ++ .../event-block/EventBlock.module.scss | 30 ++++++++----- .../rundown/event-block/EventBlock.tsx | 3 ++ .../rundown/event-block/EventBlockInner.tsx | 10 ++--- .../src/features/viewers/ViewWrapper.tsx | 3 +- .../features/viewers/countdown/Countdown.tsx | 3 +- .../viewers/minimal-timer/MinimalTimer.tsx | 4 +- .../src/features/viewers/timer/Timer.tsx | 5 ++- apps/electron/src/menu/applicationMenu.js | 9 +--- 19 files changed, 122 insertions(+), 92 deletions(-) diff --git a/apps/client/src/common/components/copy-tag/CopyTag.tsx b/apps/client/src/common/components/copy-tag/CopyTag.tsx index 54799e5fa..4306560ae 100644 --- a/apps/client/src/common/components/copy-tag/CopyTag.tsx +++ b/apps/client/src/common/components/copy-tag/CopyTag.tsx @@ -14,21 +14,18 @@ interface CopyTagProps { export default function CopyTag(props: PropsWithChildren) { 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 ( - - - } - variant='ontime-filled' - tabIndex={-1} - onClick={() => navigator.clipboard.writeText(children as string)} - /> + + + } variant='ontime-filled' tabIndex={-1} onClick={handleClick} /> ); diff --git a/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx b/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx index dac1aa50a..0fb8bf8fc 100644 --- a/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx +++ b/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx @@ -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}`; } diff --git a/apps/client/src/common/stores/appModeStore.ts b/apps/client/src/common/stores/appModeStore.ts index 5e031d190..3698443d3 100644 --- a/apps/client/src/common/stores/appModeStore.ts +++ b/apps/client/src/common/stores/appModeStore.ts @@ -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()((set) => ({ - mode: AppMode.Edit, + mode: getModeFromSession(), cursor: null, editId: null, setMode: (mode: AppMode) => set((state) => { + persistModeToSession(mode); return mode === AppMode.Edit ? { editId: state.cursor, diff --git a/apps/client/src/externals.ts b/apps/client/src/externals.ts index f1f4e63ba..64ed471ce 100644 --- a/apps/client/src/externals.ts +++ b/apps/client/src/externals.ts @@ -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'; diff --git a/apps/client/src/features/control/playback/playback-buttons/PlaybackButtons.tsx b/apps/client/src/features/control/playback/playback-buttons/PlaybackButtons.tsx index 227a225e9..5de1ae7b3 100644 --- a/apps/client/src/features/control/playback/playback-buttons/PlaybackButtons.tsx +++ b/apps/client/src/features/control/playback/playback-buttons/PlaybackButtons.tsx @@ -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 (
- setPlayback.startNext()} aspect='fill' className={styles.go}> - GO + + {goModeText}
setPlayback.start()} + onClick={setPlayback.start} disabled={isStopped || isRolling} theme={Playback.Play} active={isPlaying} @@ -53,7 +62,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) { setPlayback.pause()} + onClick={setPlayback.pause} disabled={isStopped || isRolling || isArmed} theme={Playback.Pause} active={isPaused} @@ -63,19 +72,19 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
- setPlayback.previous()} disabled={disablePrev}> + - setPlayback.next()} disabled={disableGo}> +
setPlayback.roll()} + onClick={setPlayback.roll} disabled={!isStopped || noEvents} theme={Playback.Roll} active={isRolling} @@ -83,12 +92,12 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) { - setPlayback.reload()} disabled={isStopped || isRolling}> + - setPlayback.stop()} disabled={isStopped && !isRolling} theme={Playback.Stop}> + diff --git a/apps/client/src/features/event-editor/EventEditorExport.tsx b/apps/client/src/features/event-editor/EventEditorExport.tsx index 587a2cd0d..f6fb60797 100644 --- a/apps/client/src/features/event-editor/EventEditorExport.tsx +++ b/apps/client/src/features/event-editor/EventEditorExport.tsx @@ -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 ( @@ -33,13 +31,7 @@ const EventEditorExport = () => {
- } - onClick={removeOpenEvent} - isDisabled={!canRemoveOpenId} - {...closeBtnStyle} - /> + } onClick={removeOpenEvent} {...closeBtnStyle} />
diff --git a/apps/client/src/features/modals/ModalLink.tsx b/apps/client/src/features/modals/ModalLink.tsx index 5e30a183e..3e6a6b6a6 100644 --- a/apps/client/src/features/modals/ModalLink.tsx +++ b/apps/client/src/features/modals/ModalLink.tsx @@ -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 ( - + {children} ); diff --git a/apps/client/src/features/modals/about-modal/AboutModal.tsx b/apps/client/src/features/modals/about-modal/AboutModal.tsx index cc0585000..90921b030 100644 --- a/apps/client/src/features/modals/about-modal/AboutModal.tsx +++ b/apps/client/src/features/modals/about-modal/AboutModal.tsx @@ -43,7 +43,7 @@ export default function AboutModal(props: AboutModalProps) {
Ontime Free Open Source Software for managing rundowns and event timers - www.getontime.no + www.getontime.no
Current version diff --git a/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx b/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx index fc177c9b4..a08ccbf9f 100644 --- a/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx +++ b/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx @@ -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; diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index df58f5191..8cddcd442 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -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(); + const cursorRef = useRef(null); + const scrollRef = useRef(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, + scrollRef: MutableRefObject, + ) { + 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, scrollRef as MutableRefObject); + }); + } + + // 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 ( -
+
@@ -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 (
{ previousEnd={previousEnd} /> + {next && ( + + UP NEXT + + )} { {selected && }
- {next && ( - - NEXT - - )} diff --git a/apps/client/src/features/viewers/ViewWrapper.tsx b/apps/client/src/features/viewers/ViewWrapper.tsx index b2565b689..a440bb3fa 100644 --- a/apps/client/src/features/viewers/ViewWrapper.tsx +++ b/apps/client/src/features/viewers/ViewWrapper.tsx @@ -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 =

(Component: ComponentType

) => { // get clock string const TimeManagerType = { ...timer, - finished: playback === Playback.Play && (timer.current ?? 0) < 0 && timer.startedAt, playback, }; diff --git a/apps/client/src/features/viewers/countdown/Countdown.tsx b/apps/client/src/features/viewers/countdown/Countdown.tsx index a3fc38f9d..7a108fd25 100644 --- a/apps/client/src/features/viewers/countdown/Countdown.tsx +++ b/apps/client/src/features/viewers/countdown/Countdown.tsx @@ -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' : ''; diff --git a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx index 54beb3e1a..aa7f102ab 100644 --- a/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx +++ b/apps/client/src/features/viewers/minimal-timer/MinimalTimer.tsx @@ -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; diff --git a/apps/client/src/features/viewers/timer/Timer.tsx b/apps/client/src/features/viewers/timer/Timer.tsx index eb01ddd2b..35b51accf 100644 --- a/apps/client/src/features/viewers/timer/Timer.tsx +++ b/apps/client/src/features/viewers/timer/Timer.tsx @@ -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) { /> - {title.showNow && !time.finished && ( + {title.showNow && !finished && ( { - 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/'); }, }, ],