From 0057b270ec51ad1bd5bb6659d2fc24b296df5d90 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 28 Nov 2023 12:43:16 +0100 Subject: [PATCH] feat: add RundownScript view, UIStore --- .../client/src/RundownList/RundownEntry.tsx | 13 ++- .../client/src/RundownList/RundownList.tsx | 4 +- .../RundownScript/RundownScript.module.scss | 3 + .../src/RundownScript/RundownScript.tsx | 39 ++++++++ .../SplitPanel/SplitPanel.module.css | 28 ++++++ .../src/components/SplitPanel/SplitPanel.tsx | 96 +++++++++++++++++++ packages/apps/client/src/index.scss | 4 +- packages/apps/client/src/main.tsx | 5 + packages/apps/client/src/stores/AppStore.ts | 3 + .../apps/client/src/stores/RundownStore.ts | 47 ++++----- packages/apps/client/src/stores/UIStore.ts | 13 +++ 11 files changed, 217 insertions(+), 38 deletions(-) create mode 100644 packages/apps/client/src/RundownScript/RundownScript.module.scss create mode 100644 packages/apps/client/src/RundownScript/RundownScript.tsx create mode 100644 packages/apps/client/src/components/SplitPanel/SplitPanel.module.css create mode 100644 packages/apps/client/src/components/SplitPanel/SplitPanel.tsx create mode 100644 packages/apps/client/src/stores/UIStore.ts diff --git a/packages/apps/client/src/RundownList/RundownEntry.tsx b/packages/apps/client/src/RundownList/RundownEntry.tsx index b6a18f3..67510fa 100644 --- a/packages/apps/client/src/RundownList/RundownEntry.tsx +++ b/packages/apps/client/src/RundownList/RundownEntry.tsx @@ -2,16 +2,17 @@ import React from 'react' import { observer } from 'mobx-react-lite' import { AppStore } from '../stores/AppStore' import { UIRundownId } from '../model/UIRundown' -import { action } from 'mobx' import { Button } from 'react-bootstrap' +import { useNavigate } from 'react-router-dom' -const RundownEntry = observer(({ rundownId }: { rundownId: UIRundownId }): React.JSX.Element => { +export const RundownEntry = observer(({ rundownId }: { rundownId: UIRundownId }): React.JSX.Element => { const rundownEntry = AppStore.rundownStore.allRundowns.get(rundownId) + const navigate = useNavigate() - const onOpen = action(() => { + const onOpen = () => { if (!rundownEntry) return - AppStore.rundownStore.loadRundown(rundownEntry.playlistId) - }) + navigate(`/rundown/${rundownEntry.playlistId}`) + } return (

@@ -20,5 +21,3 @@ const RundownEntry = observer(({ rundownId }: { rundownId: UIRundownId }): React ) }) RundownEntry.displayName = 'RundownEntry' - -export { RundownEntry } diff --git a/packages/apps/client/src/RundownList/RundownList.tsx b/packages/apps/client/src/RundownList/RundownList.tsx index c50b70f..c1c8022 100644 --- a/packages/apps/client/src/RundownList/RundownList.tsx +++ b/packages/apps/client/src/RundownList/RundownList.tsx @@ -5,7 +5,7 @@ import { AppStore } from '../stores/AppStore' import { UIRundownId } from '../model/UIRundown' import { RundownEntry } from './RundownEntry' -const RundownList = observer((): React.JSX.Element => { +export const RundownList = observer((): React.JSX.Element => { const allRundownIds = keys(AppStore.rundownStore.allRundowns) return ( @@ -21,5 +21,3 @@ const RundownList = observer((): React.JSX.Element => { ) }) RundownList.displayName = 'RundownList' - -export { RundownList } diff --git a/packages/apps/client/src/RundownScript/RundownScript.module.scss b/packages/apps/client/src/RundownScript/RundownScript.module.scss new file mode 100644 index 0000000..aadcd3b --- /dev/null +++ b/packages/apps/client/src/RundownScript/RundownScript.module.scss @@ -0,0 +1,3 @@ +.RundownScript { + height: 100vh; +} diff --git a/packages/apps/client/src/RundownScript/RundownScript.tsx b/packages/apps/client/src/RundownScript/RundownScript.tsx new file mode 100644 index 0000000..d28ccad --- /dev/null +++ b/packages/apps/client/src/RundownScript/RundownScript.tsx @@ -0,0 +1,39 @@ +import { useEffect } from 'react' +import { observer } from 'mobx-react-lite' +import classes from './RundownScript.module.scss' +import { CurrentRundown } from '../CurrentRundown/CurrentRundown' +import { ScriptEditor } from '../ScriptEditor/ScriptEditor' +import { Helmet } from 'react-helmet-async' +import { RundownPlaylistId, protectString } from '@sofie-prompter-editor/shared-model' +import { AppStore } from '../stores/AppStore' +import { useParams } from 'react-router-dom' +import { SplitPanel } from '../components/SplitPanel/SplitPanel' + +export const RundownScript = observer((): React.JSX.Element => { + const params = useParams() + + const playlistId = protectString(params.playlistId) + + useEffect(() => { + if (!playlistId) return + + AppStore.rundownStore.loadRundown(playlistId) + }, [playlistId]) + + return ( + <> + + Rundown + + + AppStore.uiStore.setViewDividerPosition(e.value)} + className={classes.RundownScript} + childrenBegin={} + childrenEnd={} + /> + + ) +}) +RundownScript.displayName = 'RundownScript' diff --git a/packages/apps/client/src/components/SplitPanel/SplitPanel.module.css b/packages/apps/client/src/components/SplitPanel/SplitPanel.module.css new file mode 100644 index 0000000..61ab30f --- /dev/null +++ b/packages/apps/client/src/components/SplitPanel/SplitPanel.module.css @@ -0,0 +1,28 @@ +.SplitPane { + --divider-width: 5px; + display: grid; + grid-template-rows: auto; + grid-template-columns: [PaneA] var(--position-a, 1fr) [Divider] var(--divider-width) [PaneB] var(--position-b, 1fr); + gap: 2px; +} + +.PaneA { + grid-column: PaneA; + overflow: auto; +} + +.Divider { + grid-column: Divider; + --test: 1; + cursor: ew-resize; +} + +.Divider:hover, +.DividerActive { + background-color: var(--bs-primary); +} + +.PaneB { + grid-column: PaneB; + overflow: auto; +} diff --git a/packages/apps/client/src/components/SplitPanel/SplitPanel.tsx b/packages/apps/client/src/components/SplitPanel/SplitPanel.tsx new file mode 100644 index 0000000..16ef111 --- /dev/null +++ b/packages/apps/client/src/components/SplitPanel/SplitPanel.tsx @@ -0,0 +1,96 @@ +import { ReactNode, useEffect, useMemo, useRef, useState } from 'react' +import classes from './SplitPanel.module.css' + +export function SplitPanel({ + position, + onChange, + childrenBegin, + childrenEnd, + className, +}: { + className?: string + position?: number + onChange?: ChangeEventHandler + childrenBegin: ReactNode + childrenEnd: ReactNode + children?: null +}) { + const [isResizing, setIsResizing] = useState(false) + const beginCoords = useRef<{ x: number; y: number } | null>(null) + const initialPos = useRef(position ?? 0.5) + const container = useRef(null) + const contRect = useRef(null) + + const defaultedPosition = position ?? 0.5 + + function onMouseDown(e: React.MouseEvent) { + setIsResizing(true) + beginCoords.current = { x: e.clientX, y: e.clientY } + contRect.current = container.current?.getBoundingClientRect() ?? null + } + + const style = useMemo(() => { + const fract = Math.max(0, Math.min(1, defaultedPosition)) + return { + '--position-a': `${fract}fr`, + '--position-b': `${1 - fract}fr`, + } as React.CSSProperties + }, [defaultedPosition]) + + useEffect(() => { + if (!isResizing) return + + function onMouseMove(e: MouseEvent) { + if (!beginCoords.current || !contRect.current) return + + const diffX = (e.clientX - beginCoords.current.x) / contRect.current.width + const diffY = (e.clientY - beginCoords.current.y) / contRect.current.height + + const newValue = Math.max(0, Math.min(1, initialPos.current + diffX)) + + onChange?.({ + value: newValue, + }) + + e.preventDefault() + } + + function onMouseUp(e: MouseEvent) { + setIsResizing(false) + } + + window.addEventListener('mousemove', onMouseMove, { + capture: true, + }) + window.addEventListener('mouseup', onMouseUp, { + once: true, + capture: true, + }) + + return () => { + window.removeEventListener('mousemove', onMouseMove, { + capture: true, + }) + window.removeEventListener('mouseup', onMouseUp, { + capture: true, + }) + } + }, [isResizing, onChange]) + + useEffect(() => { + if (isResizing) return + + initialPos.current = defaultedPosition + }, [isResizing, defaultedPosition]) + + return ( +

+
{childrenBegin}
+
+
{childrenEnd}
+
+ ) +} + +type ChangeEvent = { value: number } +type ChangeEventHandler = (e: ChangeEvent) => void diff --git a/packages/apps/client/src/index.scss b/packages/apps/client/src/index.scss index 88c6cfe..dce3267 100644 --- a/packages/apps/client/src/index.scss +++ b/packages/apps/client/src/index.scss @@ -1,6 +1,8 @@ /* The following line can be included in a src/App.scss */ $primary: #1769ff; -$dark: #252627; +//$dark: #252627; // this is Origo's original dark color +$dark: #1d1d1d; +$body-bg-dark: $dark; $font-family-sans-serif: Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; diff --git a/packages/apps/client/src/main.tsx b/packages/apps/client/src/main.tsx index 5a8d19c..081e64c 100644 --- a/packages/apps/client/src/main.tsx +++ b/packages/apps/client/src/main.tsx @@ -7,8 +7,13 @@ import { MobXPlayground } from './MobXPlayground/MobXPlayground.tsx' import { BackendPlayground } from './BackendPlayground/BackendPlayground.tsx' import { ScriptEditor } from './ScriptEditor/ScriptEditor.tsx' import { HelmetProvider } from 'react-helmet-async' +import { RundownScript } from './RundownScript/RundownScript.tsx' const router = createBrowserRouter([ + { + path: '/rundown/:playlistId', + element: , + }, { path: '/', element: , diff --git a/packages/apps/client/src/stores/AppStore.ts b/packages/apps/client/src/stores/AppStore.ts index da0f236..8594712 100644 --- a/packages/apps/client/src/stores/AppStore.ts +++ b/packages/apps/client/src/stores/AppStore.ts @@ -1,15 +1,18 @@ import { makeAutoObservable, action } from 'mobx' import { RundownStore } from './RundownStore' import { MockConnection } from '../mocks/mockConnection' +import { UIStore } from './UIStore' class AppStoreClass { connected = false rundownStore: RundownStore + uiStore: UIStore connection = new MockConnection() constructor() { makeAutoObservable(this) this.rundownStore = new RundownStore(this, this.connection) + this.uiStore = new UIStore() this.connection.on( 'connected', diff --git a/packages/apps/client/src/stores/RundownStore.ts b/packages/apps/client/src/stores/RundownStore.ts index 04767c3..2ff58bb 100644 --- a/packages/apps/client/src/stores/RundownStore.ts +++ b/packages/apps/client/src/stores/RundownStore.ts @@ -1,4 +1,4 @@ -import { makeAutoObservable, observable, action } from 'mobx' +import { makeAutoObservable, observable, action, flow } from 'mobx' import { RundownPlaylistId } from '@sofie-prompter-editor/shared-model' import { APIConnection, AppStore } from './AppStore' import { UIRundown, UIRundownId } from '../model/UIRundown' @@ -22,21 +22,18 @@ export class RundownStore { this.loadAllRudnowns() } - loadAllRudnowns() { - this.connection.playlist.find().then( - action('receiveRundowns', (playlists) => { - // add UIRundownEntries to allRundowns + loadAllRudnowns = flow(function* (this: RundownStore) { + const playlists = yield this.connection.playlist.find() + // add UIRundownEntries to allRundowns - this.clearAllRundowns() + this.clearAllRundowns() - for (const playlist of playlists) { - const newRundownEntry = new UIRundownEntry(this, playlist._id) - this.allRundowns.set(newRundownEntry.id, newRundownEntry) - newRundownEntry.updateFromJson(playlist) - } - }) - ) - } + for (const playlist of playlists) { + const newRundownEntry = new UIRundownEntry(this, playlist._id) + this.allRundowns.set(newRundownEntry.id, newRundownEntry) + newRundownEntry.updateFromJson(playlist) + } + }) clearAllRundowns() { for (const rundown of this.allRundowns.values()) { @@ -44,21 +41,17 @@ export class RundownStore { } } - loadRundown(id: RundownPlaylistId) { + loadRundown = flow(function* (this: RundownStore, id: RundownPlaylistId) { this.openRundown?.close() // get a full rundown from backend and create a UIRundown object // assign to openRundown - this.connection.playlist.get(id).then( - action('receiveRundown', (playlist) => { - if (!playlist) { - console.error('Playlist not found!') - return - } + const playlist = yield this.connection.playlist.get(id) + if (!playlist) { + throw new Error('Playlist not found') + } - const newRundown = new UIRundown(this, playlist._id) - newRundown.updateFromJson(playlist) - this.openRundown = newRundown - }) - ) - } + const newRundown = new UIRundown(this, playlist._id) + newRundown.updateFromJson(playlist) + this.openRundown = newRundown + }) } diff --git a/packages/apps/client/src/stores/UIStore.ts b/packages/apps/client/src/stores/UIStore.ts new file mode 100644 index 0000000..1302ace --- /dev/null +++ b/packages/apps/client/src/stores/UIStore.ts @@ -0,0 +1,13 @@ +import { makeAutoObservable } from 'mobx' + +export class UIStore { + viewDividerPosition: number = 0.5 + + constructor() { + makeAutoObservable(this, {}) + } + + setViewDividerPosition(value: number) { + this.viewDividerPosition = value + } +}