Compare commits

..

99 Commits

Author SHA1 Message Date
Carlos Valente 672267c0b3 v2 beta6 (#379)
* feat: operator key

* chore: update demo

* chore: version bump

* fix: prevent stale keys

* refactor: add new fields to validation

* style: tweaks to modal arrangement

* refactor: prevent parsing http

* ux: click anywhere in event to edit

* refactor: performance improvements to time input

* refactor: performance improvements menu
2023-05-13 23:36:19 +02:00
Carlos Valente ca65d31071 Ux language (#378)
* ux: language from menu

* feat: add extra support for languages

german
portuguese
spanish

* refactor: type improvements
2023-05-12 23:16:20 +02:00
Carlos Valente 305d6b6476 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
2023-05-12 22:31:26 +02:00
Carlos Valente 139b667e20 v2 beta5 modals (#377)
* refactor: folder structure

* style: modal wrapper

* feat: add nordic translations
2023-05-12 22:16:33 +02:00
Fabian Posenau 29a9167d61 Feat/timer/stylechange (#341)
* feat: multi color progress bar

* add color support to minimal timer

Co-Authored-By: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>
2023-05-03 20:16:35 +02:00
Carlos Valente 38654f8981 Feature: quick start (#369)
* feat: quick start modal

* refactor: end message is part of view settings

* refactor: small tweaks and type improvements
2023-04-29 21:20:25 +02:00
Carlos Valente 33b496f98e v2 beta 4 bump (#367)
* chore: update links to documentation

* chore: version bump
2023-04-28 20:48:06 +02:00
Carlos Valente 10673b3ee1 ux: content aware playback (#364) 2023-04-26 22:07:26 +02:00
Carlos Valente 4bceae65ab fix v2 roll timer (#363)
* fix: roll update skips elapsed time

* refactor: remove unused data

* style: fix style in roll state
2023-04-26 22:02:04 +02:00
Carlos Valente 40d389acc9 v2 beta 4 (#361)
* ux: rename end action

* chore: update demo db

* style: tweaks on extracted rundown

* chore: prevent console logs in production code

* feat: go mode

* style: remove window size limits

* style: rename delete action

* style: small tweaks on icons

* fix: style override on params

* chore: update test db

* refactor: small code quality improvements

* refactor: prevent circular imports
2023-04-25 22:35:14 +02:00
Carlos Valente 0b94fc71a4 v2 about (#362)
* chore: remove unused assets

* feat: about ontime

* refactor: cleanup unused
2023-04-25 22:31:56 +02:00
Carlos Valente 8f834fed35 style: cuesheet block (#355)
* style: cuesheet show block title

* style: cuesheet show full delay string
2023-04-23 10:18:19 +02:00
Carlos Valente db9cb61164 fix: osc modal (#352)
* refactor: parse time input

* fix: form dirty flag on dynamic elements
2023-04-21 20:06:40 +02:00
Carlos Valente 9c4c84905e feat: edit mode (#344)
* style: add mode selection to menu

* style: rename rundown functions

* feat: edit mode
2023-04-21 20:06:31 +02:00
Carlos Valente da889a4825 chore: vite upgrade (#354)
* chore: upgrade vite
2023-04-21 20:06:12 +02:00
Carlos Valente 4ca9fb9f72 refactor: parse time input (#351) 2023-04-21 09:20:34 +02:00
Carlos Valente b228020b73 V2 e2e playwright (#343)
* test: create utility for uploading test-db

* chore: upgrade deps

* refactor: remove unused functions

* chore: create testing boilerplate

* test: upload showfile
2023-04-18 23:16:19 +02:00
cv f248b78945 refactor: changes in time input 2023-04-18 23:15:15 +02:00
Carlos Valente 11663e5416 refactor: serve react as compressed br (#339) 2023-04-15 13:27:29 +02:00
Carlos Valente 6732314f15 V2 fix delay (#340)
* fix: avoid modifying original objects
2023-04-15 13:26:57 +02:00
cv e9fe0c0a3a fix: resolve to static port in dev mode 2023-04-15 12:46:17 +02:00
Carlos Valente 770d12888d V2 beta3 (#338)
* chore: upgrade local build documentation

* style: keep scrolling event in screen

* feat: delay is time entry

* refactor: remove unused

* refactor: remove unused

* refactor: batch store updates

* refactor: virtually remove cap on events

* refactor: style and behaviour tweaks to event block

* style: tweaks on schedules

* chore: remove sentry from server

* style: reorder menu

* chore: update docs
2023-04-14 10:13:46 +02:00
Fabian Posenau 94a1369d64 fix docker on different port (#337)
Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-04-14 10:08:42 +02:00
Fabian Posenau 5ba27dca1d add onClick handler (#335)
Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-04-10 20:00:09 +02:00
cv 3df17d172f fix: sentry hangs node process 2023-04-09 19:54:19 +02:00
Fabian Posenau d59c92219f Fix/events enpoint (#329) 2023-04-09 13:53:51 +02:00
cv 23fce763e5 style: ensure events are always visible 2023-04-06 23:39:08 +02:00
cv 524aa74a3d chore: upgrade deps 2023-04-06 20:56:01 +02:00
cv 32b7908d5b chore: upgrade deps 2023-04-06 20:50:56 +02:00
cv e36b19a511 style: small tweaks on blocks 2023-04-06 20:46:24 +02:00
cv ba62276951 refactor: improve performance on EventBlock 2023-04-06 14:42:34 +02:00
cv 4c4b83979d fix: close editor on event delete 2023-04-06 14:10:23 +02:00
cv 2dcea86b36 style: small style tweaks 2023-04-06 14:07:19 +02:00
cv e54996bd5e refactor: prevent iterating on undefined 2023-04-06 11:58:47 +02:00
cv 41753fc202 chore: upgrade deps 2023-04-06 11:47:30 +02:00
cv e77572c0b6 refactor: performance tweaks to event editor 2023-04-06 11:00:28 +02:00
cv 4c268d531e style: fix info scroll 2023-04-06 09:49:57 +02:00
Carlos Valente 5e481d49da V2 beta1 (#326)
* style: labels on added time

* style: remove mentions of PiP

* refactor: unify usage of ms for timers

* refactor: create events with 0 duration

* style: several small tweaks

* refactor: keep block when applying delays

* feat: blocks have titles

* style: improvements in time entry warnings

* style: override progress bar styles

* style: prevent overflow

* feat: show character count in editor

* refactor: provide initial payload

* refactor: lower test boundary

* refactor: get colour from swatches

* style: rename title block

* chore: version bump
2023-04-05 22:08:13 +02:00
Carlos Valente 0a68377b82 feat: notify ws clients on rundown changes (#327) 2023-04-05 21:42:18 +02:00
Carlos Valente cb39a13bea Add translations for views (#325) (#328)
* Add translations for views (#325)

---------

Co-authored-by: Kevin <60135953+kev-ac@users.noreply.github.com>
2023-04-05 21:41:50 +02:00
Carlos Valente e56460a940 V2 fix roll (#323)
* chore: profile node

* fix: always roll
2023-04-04 10:22:28 +02:00
Carlos Valente 390d466673 Update build_docker_v2.yml 2023-04-03 11:50:55 +02:00
Carlos Valente b778835469 refactor: ensure start order (#322) 2023-04-03 11:10:51 +02:00
Carlos Valente 32b5ab0574 V2 align (#321)
* fix: fetch in offline environments (#295)

* Add arm platforms to docker build (#297)

* add arm platforms to docker build

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* fix: docker build (#298)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* Timer: fix too many renders error when using ?progress (#305)

* Timer: fix too many renders error when using ?progress

* version bump (#314)

* style: small tweaks

* fix: prevent actions in roll mode

* chore: update external assets

* style: show shortcuts in quick blocks

* refactor: revert optimisations on callbacks

* fun: industry dictionary

* style: indicators for event features

---------

Co-authored-by: Fabian Posenau <fabian.p99@gmx.de>
Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: Marks Polakovs <github@markspolakovs.me>
2023-04-03 09:03:36 +02:00
Carlos Valente 03e7428348 V2 e2e (#320)
* chore: cleanup handling of a bad read

* chore: configure e2e testing

* chore: version bump
2023-04-02 19:07:13 +02:00
Carlos Valente 0e47b5c10c Update test_v2.yml 2023-04-02 13:30:24 +02:00
Carlos Valente 6582f4a9bc Update test_v2.yml 2023-04-02 13:24:07 +02:00
Carlos Valente 7456205b6e V2 build (#319)
* chore: build pipeline for v2

* chore: run unit tests

* chore: configure sentry build
2023-04-01 23:06:42 +02:00
Carlos Valente 9338a615d4 v2 lazy rundown (#317)
* chore: update related deps

* refactor: memoise entrypoints

* refactor: store selectors

* refactor: improve component performance

* chore: remove unused dependencies

* refactor: improve component dnd
2023-03-30 21:22:36 +02:00
Fabian Posenau 03d5389540 V2 docker (#312)
* Create docker-ci.yml

* docker build

* add build app

* path changes

* delete double workflow

* update docker run template

* cleanup

* resolve wierd path issue

* wrong path

* again wrong path

* rename docker releases

* move styles population and change to function call

* re-add old build pipeline

* rename buld file

* feractor init function

* refactor: startDb to initAssets

* refactor: startDB to initAssets

* fix: docker.cjs building whole app

* fix: docker environment

* update docker paths

* update: build command

* cleanup

* cleanup: commented code

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-03-28 20:12:44 +02:00
Carlos Valente bca7ad30b1 v2 alpha5 (#315)
* fix: fetch in offline environments (#295)

* add arm platforms to docker build

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* fix: docker build (#298)

Co-authored-by: Fabian Posenau <fabian@fphome.de>

* Timer: fix too many renders error when using ?progress (#305)

* ux: remove duplicate showing of selected event

* several small fixes and UX improvements
---------

Co-authored-by: Fabian Posenau <fabian.p99@gmx.de>
Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: Marks Polakovs <github@markspolakovs.me>
2023-03-24 08:19:47 +01:00
Carlos Valente e937af62b1 V2 osc (#313)
* chore: upgrade relevant deps

* fix: electron app to tray

* chore: cleanup dictionary

* feat: handle several messages in a event

* ux: disable irrelevant buttons in browser

* feat: parse and validate subscriptions

* feat: create UI for OSC Integration

* fix: cleanup logger behaviour

* feat: allow OSC settings to be changed at runtime
2023-03-23 08:33:22 +01:00
Fabian Posenau 73533600a0 Feat/end action (#308)
* feat: end action

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>
2023-03-16 19:27:15 +01:00
Carlos Valente 76c8f8a4d5 V2 ws store (#310)
* Update TimerService.ts

* refactor: message service publishes to store

* refactor: several type improvements

* V2 ws store wss (#309)

* refactor: shared logging types

* refactor: simplify message service consumption

* refactor: create discrete logging system

* refactor: move socket.io > websocket
2023-03-15 22:06:47 +01:00
Fabian Posenau 11648ee546 improve time input (#307)
* add the ability to parse time like 2h20m

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-03-06 19:47:09 +01:00
Fabian Posenau 5b977790b0 Feat/update check (#303)
* feat: show latest version in settings modal

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-03-04 22:40:19 +01:00
Carlos Valente 3e2626d4f9 V2 fix timer (#304) 2023-02-28 19:50:37 +01:00
Fabian Posenau 99f4738f4a fix: wrong parameter (#302)
Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-02-26 20:06:40 +01:00
Fabian Posenau c405698813 Feat/countdown style (#290)
* feat: Countdown Style

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com>
2023-02-26 19:53:26 +01:00
Fabian Posenau 6d138ce9f1 fix: backgroundcolor for .blockNotes (#300)
Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-02-25 17:41:42 +01:00
Carlos Valente 068c72662d style: alpha4 style tweaks (#299) 2023-02-25 17:27:47 +01:00
Carlos Valente edac1d7f75 V2 fix socket (#296)
* refactor: rename timer endpoint

* refactor: typescript

* refactor: rename eventData and viewSettings

* refactor: message manager uses event store
2023-02-24 22:47:07 +01:00
Carlos Valente 03552056cb V2 fix offline (#294)
* fix: resolve path to UI build in dev

* fix: configure offline fetching
2023-02-23 21:10:41 +01:00
Carlos Valente 70cc071425 V2 integration (#293)
* feat: integrations and lifecycles

* refactor: prevent issues with start order
2023-02-23 20:22:38 +01:00
Carlos Valente d8a84ac88d V2 types (#291) 2023-02-20 18:28:54 +01:00
Fabian Posenau 63d763cbaf Feature/#276/qrcodes (#289)
* bugfix: wrong format when on 12h format

* update: website link in placeholder

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
2023-02-19 20:52:42 +01:00
Carlos Valente df351b8cba hotfix: resolve data directory (#288) 2023-02-17 20:23:00 +01:00
Fabian Posenau 298af3ec5a Fix: #279 End Message in Minimal Timer (#286)
* fix: Minimal View #279 end message
---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-02-16 21:46:40 +01:00
Carlos Valente de9a7a87fd V2 monorepo (#285)
* refactor(project structure): UI

* refactor(project structure): extract utilities

* refactor(project structure): remove unused

* refactor(project structure): electron

* refactor(project structure): server

refactor: migrate to vitest

refactor: monorepo config

* refactor: extract application menu

* refactor: exit process

* refactor: extract tray menu

* chore: electron build

* Added Seconds in studio clock #282
---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

---------

Co-authored-by: Fabian Posenau <fabian.p99@gmx.de>
Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-02-14 22:02:15 +01:00
Carlos Valente 3918758d32 v2: roll (#280)
* ux: delete flow

* style: small tweaks in interface

* refactor: gracefully quit on error

* fix: logic around updating events

* chore: cleanup dictionary

* refactor: improve DX on creating aux files

* fix: safe destructure function return

* refactor: safe handling of falsy timer values

* fix: improve ux on stopping roll mode

* chore: upgrade deps

* feat: roll mode

* refactor: remove unused
2023-01-07 22:44:21 +01:00
Carlos Valente 832c060940 v2: views (#278)
* config(sentry): instrumentation only in production

* chore: upgrade deps

* refactor: simplify and convert to ts

* refactor: composition and styles

* chore: remove deprecated deps

* ux: close menu on click outside

* style: small tweaks and clarifications

* refactor: retire PiP view

* ux: small tweaks

* style: cleanup deprecated styles

* chore: remove unused

* refactor: remove usages of deprecated fields and improve null timer presentation

* refactor: improve composition of countdown

* style: cleanup overridable styles

* ux: show event overtime status

* style: correct logic in paused timers
2022-12-30 11:41:03 +01:00
Carlos Valente 110d3fb7a3 v2 event timer (#274)
* lint: cleanup unused

* refactor: rename component to avoid conflict

* style: gap in element row

* refactor: rename playstate -> playback

* refactor: cleanup usages of timer and prepare integration manager

* style: re-arrange button order

* refactor: improve event loading

* feat(timer-service): hot reload

* fix: issue with duration input

* chore: cleanup debug

* refactor: resolve poll from runtime store

* refactor: cleanup merge

* refactor: small improvements in timer hot-reload
2022-12-26 12:58:33 +01:00
cv 062ff7226a Merge remote-tracking branch 'origin/master' into v-2 2022-12-26 09:47:18 +01:00
Carlos Valente 6f634c36f9 v2 alpha 3 (#272)
* style: correct background colours

* chore: add folder resolutions to vite

* refactor: add auto refetch to HTTP APIs

* fix: view settings override endpoint

* refactor: memoize callbacks

* fix: reorder reaching wrong data adapter

* refactor: memoize callbacks

* style: presentation cleanup

* fix(delete): prevent flow with deleted event in playback

* refactor: convert to typescript

* refactor: small fixes and typescript conversion

* ux: improve feedback on local changes

* refactor: cleanup props
2022-12-26 09:40:33 +01:00
Carlos Valente 1bb67eb82a V2 sentry (#267)
* chore: add sentry to frontend

* chore: add sentry to server

* chore: report only in production

* chore: add sentry source maps
2022-12-08 22:34:17 +01:00
Carlos Valente 6720626bd3 v2 event editor (#265)
* style: add cursor indicator in all blocks

* style: prevent reflows on width

* style: harmonise white range

* ux: focus elements on cursor nav

* ux: prevent focus on indicators

* refactor: convert to typescript

* fix: missing dependency

* chore: add linter rules for dependency arrays

* style: style editable component

* style: tweak input styles

* style: tweak switch styles

* style: style tweaks
2022-12-08 22:22:08 +01:00
Carlos Valente c56c5a636d v2: refactor (#264)
* chore: upgrade dependencies

* chore: remove unused

* fix: issue with missing index data

* fix: discrepancy on counting events

* fix: swapped classes

* fix: prevent attempting load in empty list

* chore: cleanup completed

* chore: remove unused

* refactor: convert to typescript

* refactor: migrate to new endpoints

* refactor: convert to typescript

* fix: prevent issue with undefined process

* fix: small code smells

* fix: issue with missing version variable on build

* refactor: configure retries on data fetching

* style: design review

* fix: prevent cursor out of range

* lint: react query linting

* style: checkbox styles
2022-12-07 09:58:11 +01:00
Carlos Valente d486d78594 v2 styling (#263)
* feat: implement progress bar on running event

* refactor: use event action hook

* refactor: align backend data

* style: v2 style tweaks

* style: v2 style tweaks + ts <Info>

* fix: fix delay increment calls

* style: v2 style tweaks + ts <Playback>

* style: v2 style tweaks + ts <EventEditor>

* style: v2 style tweaks + ts <MessageControl>

* fix: naming issue with message control socket endpoint

* chore: upgrade style dependencies

* style: v2 style tweaks + ts <MenuBar>

* style: v2 style tweaks + ts <RundownMenu>

* style: v2 style tweaks + ts <Playback>

* fix: add selected and next information to EventTimer

* style: v2 style tweaks + ts <EventBlock>

* style: v2 style tweaks + ts <DelayBlock>

* style: v2 style tweaks + ts <BlockBlock>

* style: v2 style tweaks + ts <QuickEntry>

* refactor: extract <TextInput> logic

* chore: upgrade build dependencies

* chore: folder structure refactor

* fix: issue with dependency path in electron

* chore: update version in demo db
2022-11-27 14:56:09 +01:00
Carlos Valente 3626b5b357 V2 event block (#261)
* feat: implement progress bar on running event

* refactor: use event action hook

* refactor: align backend data
2022-11-22 22:53:52 +01:00
cv b54c7dd454 Merge branch 'v2' of https://github.com/cpvalente/ontime into v2 2022-11-22 22:02:45 +01:00
cv ed3554d033 refactor: ensure consistent routing 2022-11-22 22:02:25 +01:00
cv 139f466beb lint: reorder imports 2022-11-22 21:58:02 +01:00
cv 23b1ced3fb refactor: run test CI on master or manual run 2022-11-22 21:48:34 +01:00
cv 0f5747839c Merge remote-tracking branch 'origin/master' into v1 2022-11-22 21:47:31 +01:00
Carlos Valente ff5735fe36 refactor: v2 event loader (#260)
* refactor: extract jest config

* refactor: rename params in reoder endpoint

* refactor: avoid app exports

* refactor: small code smells

* refactor: run development server

* refactor: extract rundown service and event loader logic

* refactor: rename events > rundown

* refactor: code style

* fix: handle external change of title

* refactor: cleanup dictionary

* refactor: rename MessageService

* refactor: use eventID for operations

* refactor: migrate OSC controller to service

* refactor: migrate Socket controller to service

* refactor: migrate HTTP controller to service

* refactor: extract logic into discrete services

* refactor: remove rundown from event timer

* refactor: remove duplicate

* refactor: remove unused

* chore: update tests
2022-11-22 21:25:55 +01:00
Carlos Valente c1f87736eb V2 socket store (#254)
* refactor: create socket utility
* refactor: ensure a single subscription to topics
* refactor: replace usages of socket object with subscription store handler
2022-11-20 18:08:49 +01:00
Carlos Valente edd983dfd6 feat: V2 ontime menu (#259) 2022-11-20 15:31:07 +01:00
Carlos Valente 2a5ba90530 hotfix: fix object naming error on excel import (#252) 2022-11-13 18:57:50 +01:00
Carlos Valente c03ed07762 V2 rundown manager (#237)
* refactor: migrate eventlist > rundown
2022-10-30 21:07:18 +01:00
Carlos Valente a3f14182a4 V2 message manager (#234)
* refactor: server run script
* refactor: extract message control feature
* refactor: socket usage for message control feature
* refactor: migrate e2e testing to playwright
* chore: add feature tests
* refactor: add validation on socket controller
* chore: jest tests server logic
* chore: update tests
* chore: run playwright tests on pull_requests
2022-10-29 20:40:38 +02:00
cv 918f8517d2 Merge remote-tracking branch 'origin/master' into v1 2022-10-27 19:32:02 +02:00
Carlos Valente 54442dc9a1 V2 styles part2 (#233)
* style: create tag to display network messages
* style: create button style for playback
* style: review style for QuickEntry
* style: review style for Info panel
* style: review style for Event blocks
* style: review style for Message control panel
* style: review style for Playback control
* style: review application styles
* style: review block styles
* refactor: TapButton has active state
* style: small style tweaks
* refactor: type checks
2022-10-26 15:12:57 +02:00
Carlos Valente 1b1aced296 v2:feat (#231)
* style: element relationship
* feat: utility to add first element
* refactor: organise dir
* ux: deleting element closes drawer
* fix: issue with escaped characters
* fix: issue with overflow in events list
* fix: prevent event timer from receiving non event
* style: prevent small shift on class change
* style: visually detach event editor
* fix: entry block knows last event
* fix: skip event timer updates on non events
* fix: prevent electron OPENGL error
* style: cleanup text styles
* style: unify input styles
* refactor: simplify style and composition
* refactor: tweaks on style + ts
* refactor: add validation to events post
* fix: handle non events in event timer
* refactor: cleanup unused
2022-10-23 19:21:49 +02:00
Carlos Valente d8ee0d4f82 styles: v2 (#229)
* style: revise selected style
* style: revise editor styles
* style: revise viewer styles
* chore: update tests
2022-10-21 19:50:25 +02:00
Carlos Valente 08d2ebcc5e fix: remove problematic (#228) 2022-10-19 21:56:10 +02:00
Carlos Valente 0fea4064c3 Feat/55 v2 (#225)
* chore: upgrade relevant libraries
* feat(skip): add skip styling to paginated items
* chore: upgrade relevant libraries
* Fix: issue with text colour (#214)
* fix: adjust text colour from context
* fix: issue with wrong proptype
* fix issue with vite migration (#217)
* hotfix: 1.8.2 issues vite migration
* fix: file import options
* feat(55): styling
* refactor: remove onhover option for entry block
* refactor: relocate logging provider
* refactor: convert to typescript
* feat(55): add event editor
* refactor: extract event actions
* fix: bad import
* feat(55): redesign components
* fix: issues with not awaiting async
* refactor: replace socket with subscription
* refactor: remove unused
* style: small tweaks
* refactor: extract event actions
* refactor: cleanup debug
* refactor: chakra imports
* fix: optimistic mutations
* refactor: handle promise rejections
* refactor: validation
* fix: rq optimistic mutations
* feat: add playback feedback to block
* refactor: no optimistic adding of events
* refactor: simplify cursor state
* styles: cleanup editor style
* refactor: cleanup duration update
* fix: revert package upgrade (issues with vitest)
* fix: prevent cyclic imports
* refactor: extract data fetcher
* refactor: typescript migration
* chore: update tests
2022-10-19 21:30:25 +02:00
Carlos Valente 13be6ea2bc chore: upgrade electron (#215)
* Fix: issue with text colour (#214)
* fix: adjust text colour from context
* fix: issue with wrong proptype
* chore: upgrade electron
* chore: declare node version
* chore: upgrade node actions
2022-10-12 20:47:53 +02:00
Carlos Valente c2ed9d7634 Refactor/socket controller (#212)
* refactor(socket): dictionary cleanup
* refactor(socket): detangle socket from EventTimer
* refactor(socket): poll object
* refactor(socket): wip, extract data responsibilities to provider
* fix: issue with onAir
* refactor: create data provider and validation utils
* refactor: remove deprecated endpoint
* refactor: detangle and validate ontime controller
* refactor: detangle and validate events controller
* refactor: validate routers
* refactor: handle post failure in modals
2022-10-09 19:10:48 +02:00
Carlos Valente cd0577bf99 Refactor/ts sockets (#210)
* refactor(ts): create socket subscription service
* refactor(ts-sockets): useSubscription hook
* refactor(ts-sockets): extract colour selection to utility
2022-10-09 19:09:47 +02:00
212 changed files with 2177 additions and 2782 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 KiB

+139
View File
@@ -0,0 +1,139 @@
name: Ontime build
on:
push:
tags: [ "v1.*.*" ]
workflow_dispatch:
jobs:
build_mac:
runs-on: macos-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '14.x'
# React
- name: React - Install dependencies
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-mac
working-directory: ./server
# Release
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ./server/dist/ontime-macOS.dmg
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_win:
runs-on: windows-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
# React
- name: React - Install dependencies
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-win
working-directory: ./server
# Release
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ./server/dist/ontime-win64.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_linux:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
# React
- name: React - Install dependencies
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-linux
working-directory: ./server
# Release
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ./server/dist/ontime-linux.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -2
View File
@@ -2,7 +2,7 @@ name: Docker Image CI Ontime V2
on:
push:
tags: [ "*" ]
tags: [ "v2.*.*" ]
workflow_dispatch:
jobs:
@@ -52,5 +52,5 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
# Push is a shorthand for --output=type=registry
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:beta_${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:beta_v2
+1 -1
View File
@@ -2,7 +2,7 @@ name: Ontime build v2
on:
push:
tags: [ "*" ]
tags: [ "v2.*.*" ]
workflow_dispatch:
jobs:
+66
View File
@@ -0,0 +1,66 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: ontime_test_CI
on:
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 16
# React
- name: React - Install dependencies
run: yarn install
working-directory: ./client
- name: React - Run tests
run: yarn test:pipeline
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: Server - Install dependencies
run: yarn install
working-directory: ./server/src
# App
- name: Electron - Install dependencies
run: yarn setup
working-directory: ./server
- name: Server - run tests
run: yarn test
working-directory: ./server
# - name: Install Playwright Browsers
# run: npx playwright install --with-deps
# working-directory: ./server
#
# - name: Run Playwright tests
# run: yarn e2e
# working-directory: ./server
#
# - uses: actions/upload-artifact@v3
# if: always()
# with:
# name: playwright-report
# path: playwright-report/
# retention-days: 7
+1 -1
View File
@@ -2,7 +2,7 @@ name: Ontime test v2
on:
pull_request:
branches: '*'
branches: [ v2 ]
workflow_dispatch:
jobs:
+23 -26
View File
@@ -1,27 +1,25 @@
[![Ontime build v2](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml/badge.svg)](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml)
[![ontime_test_CI](https://github.com/cpvalente/ontime/actions/workflows/ontime_cy.yml/badge.svg)](https://github.com/cpvalente/ontime/actions/workflows/ontime_cy.yml) [![Ontime build](https://github.com/cpvalente/ontime/actions/workflows/build.yml/badge.svg)](https://github.com/cpvalente/ontime/actions/workflows/build.yml)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-green.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Documentation in Gitbook](https://badges.aleen42.com/src/gitbook_2.svg)](https://ontime.gitbook.io)
## Download the latest releases here
<div style="display: flex; justify-content: space-around">
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/mac-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/win-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/linux-download.png"/></a>
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/dockerhub.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/mac-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/win-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/></a>
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/dockerhub.png"/></a>
</div>
# Ontime
Ontime is an application for creating and managing event running order and timers.
Ontime is an application for managing event rundowns and running stage timers.
The user inputs a list of events along with scheduling and event information.
This will then populate a series of screens which are available to be rendered by any device in the Network.
A single, locally hosted central application distributes your event information over the local network.
This enables the distribution of the data to a series of views and allows integration into video and control workflows, including OBS and d3.
This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video outputs.
![App Window](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/app.jpg)
![App Window](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/app.png)
![Views](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/overview.png)
![Views](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/02_screentypes.png)
## Using Ontime
@@ -32,8 +30,8 @@ Any device with a browser in the same network can choose one of the supported vi
This is done by reaching the ontime server at the _default port 4001_ eg: `localhost:4001`
or `192.168.1.3:4001`
<br />
You can then use the menu in the top left corner to select the desired view.
The menu will be initially hidden until there is mouse interaction.
You can then use the Ontime logo in the top left corner to select the desired view.
The logo will be initially hidden until there is mouse interaction.
In the case of unattended machines or automation, it is possible to use different URL to recall
individual views and extend view settings using the URL aliases feature
@@ -64,23 +62,21 @@ More documentation is available [in our docs](https://ontime.gitbook.io)
- [x] Distribute data over network and render it in the browser
- [x] Different screen types
- Stage Timer
- Minimal Timer
- Clock
- Backstage Info
- Public Info
- Studio Clock
- Countdown
- [Make your own?](#make-your-own-viewer)
- [x] Configurable Lower Thirds
- [x] Collaborative editing with the cuesheet view
- [x] Cuesheets with user definable fields
- [x] Send live messages to different screen types
- [x] Differentiate between backstage and public data
- [x] Workflow for managing delays
- [x] Rich protocol integrations for Control and Feedback
- [x] Protocol integrations for Control and Feedback
- OSC (Open Sound Control)
- HTTP
- WebSockets
- [x] Roll mode: run standalone using the system clock
- [x] Roll mode: run independently using the system clock
- [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally
- [x] [Countdown to anything!](https://ontime.gitbook.io/v2/views/countdown): have
a countdown to any scheduled event
@@ -89,11 +85,14 @@ More documentation is available [in our docs](https://ontime.gitbook.io)
## Unopinionated
We want Ontime to be unique by targeting freelancers instead of roles.
We are not interested in forcing workflows and have made Ontime, so it is flexible to whichever way
you would like to work.
We believe most freelancers work in different fields and we want to give you a tool that you can leverage across your many environments and workflows.
We are not interested in forcing workflows and have made Ontime so, it is flexible to whichever way you would like to work.
- [x] If you want just the info screens, there is no need to use the timer!
- [x] Don't have or care for a schedule?
- [x] a single event with no data is enough to use one of the APIs and use a dynamic timer
- [x] use the order list to create a set of quick timers by setting the beginning and start
times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC or any of the other available integrations
## Rich APIs for workflow integrations
@@ -117,7 +116,7 @@ Writing a new view for the browser can be done with basic knowledge of HTML + CS
<br />
See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on
how to get you started and read the docs about
the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/ontime-apis#osc-and-websocket-api)
the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/websocket-api)
### Headless run
@@ -140,8 +139,6 @@ and use the included docker compose to get started
docker-compose up
```
Related information available [in the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi)
## Roadmap
### Continued development
-1
View File
@@ -6,7 +6,6 @@
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta name='theme-color' content='#121212' />
<meta name='ontime' content='ontime - event timer manager' />
<meta http-equiv="Content-Security-Policy" content="fullscreen 'self';">
<link rel='apple-touch-icon' href='/logo192.png' />
<link
rel='icon'
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "2.0.8",
"version": "2.0.0-beta",
"private": true,
"dependencies": {
"@chakra-ui/react": "^2.5.5",
+22 -23
View File
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { Suspense, useEffect } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClientProvider } from '@tanstack/react-query';
@@ -6,7 +6,6 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
import { AppContextProvider } from './common/context/AppContext';
import { ContextMenuProvider } from './common/context/ContextMenuContext';
import useElectronEvent from './common/hooks/useElectronEvent';
import { ontimeQueryClient } from './common/queryClient';
import { connectSocket } from './common/utils/socket';
@@ -23,19 +22,19 @@ connectSocket();
function App() {
const { isElectron, sendToElectron } = useElectronEvent();
useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
// handle held key
if (event.repeat) return;
// check if the alt key is pressed
if (event.altKey) {
if (event.code === 'KeyT') {
// ask to see debug
sendToElectron('set-window', 'show-dev');
}
const handleKeyPress = (event: KeyboardEvent) => {
// handle held key
if (event.repeat) return;
// check if the alt key is pressed
if (event.altKey) {
if (event.code === 'KeyT') {
// ask to see debug
sendToElectron('set-window', 'show-dev');
}
};
}
};
useEffect(() => {
if (isElectron) {
document.addEventListener('keydown', handleKeyPress);
}
@@ -44,24 +43,24 @@ function App() {
document.removeEventListener('keydown', handleKeyPress);
}
};
}, [isElectron, sendToElectron]);
}, []);
return (
<ChakraProvider resetCSS theme={theme}>
<QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider>
<ContextMenuProvider>
<BrowserRouter>
<div className='App'>
<ErrorBoundary>
<BrowserRouter>
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<TranslationProvider>
<AppRouter />
</TranslationProvider>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
</BrowserRouter>
</ContextMenuProvider>
</Suspense>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
</BrowserRouter>
</AppContextProvider>
</QueryClientProvider>
</ChakraProvider>
+59 -61
View File
@@ -1,4 +1,4 @@
import { lazy, Suspense, useEffect } from 'react';
import { lazy, useEffect } from 'react';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import useAliases from './common/hooks-query/useAliases';
@@ -50,72 +50,70 @@ export default function AppRouter() {
}, [data, location, navigate]);
return (
<Suspense fallback={null}>
<Routes>
<Route path='/' element={<Navigate to='/timer' />} />
<Route path='/speaker' element={<STimer />} />
<Route path='/presenter' element={<STimer />} />
<Route path='/stage' element={<STimer />} />
<Route path='/timer' element={<STimer />} />
<Routes>
<Route path='/' element={<Navigate to='/timer' />} />
<Route path='/speaker' element={<STimer />} />
<Route path='/presenter' element={<STimer />} />
<Route path='/stage' element={<STimer />} />
<Route path='/timer' element={<STimer />} />
<Route path='/minimal' element={<SMinimalTimer />} />
<Route path='/minimalTimer' element={<SMinimalTimer />} />
<Route path='/simpleTimer' element={<SMinimalTimer />} />
<Route path='/minimal' element={<SMinimalTimer />} />
<Route path='/minimalTimer' element={<SMinimalTimer />} />
<Route path='/simpleTimer' element={<SMinimalTimer />} />
<Route path='/clock' element={<SClock />} />
<Route path='/clock' element={<SClock />} />
<Route path='/countdown' element={<SCountdown />} />
<Route path='/countdown' element={<SCountdown />} />
<Route path='/sm' element={<SBackstage />} />
<Route path='/backstage' element={<SBackstage />} />
<Route path='/sm' element={<SBackstage />} />
<Route path='/backstage' element={<SBackstage />} />
<Route path='/public' element={<SPublic />} />
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
<Route path='/public' element={<SPublic />} />
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
{/*/!* Protected Routes *!/*/}
<Route path='/editor' element={<Editor />} />
<Route path='/cuesheet' element={<Table />} />
<Route path='/cuelist' element={<Table />} />
<Route path='/table' element={<Table />} />
{/*/!* Protected Routes *!/*/}
<Route path='/editor' element={<Editor />} />
<Route path='/cuesheet' element={<Table />} />
<Route path='/cuelist' element={<Table />} />
<Route path='/table' element={<Table />} />
{/*/!* Protected Routes - Elements *!/*/}
<Route
path='/rundown'
element={
<EditorFeatureWrapper>
<RundownPanel />
</EditorFeatureWrapper>
}
/>
<Route
path='/timercontrol'
element={
<EditorFeatureWrapper>
<TimerControl />
</EditorFeatureWrapper>
}
/>
<Route
path='/messagecontrol'
element={
<EditorFeatureWrapper>
<MessageControl />
</EditorFeatureWrapper>
}
/>
<Route
path='/info'
element={
<EditorFeatureWrapper>
<Info />
</EditorFeatureWrapper>
}
/>
{/*/!* Send to default if nothing found *!/*/}
<Route path='*' element={<STimer />} />
</Routes>
</Suspense>
{/*/!* Protected Routes - Elements *!/*/}
<Route
path='/rundown'
element={
<EditorFeatureWrapper>
<RundownPanel />
</EditorFeatureWrapper>
}
/>
<Route
path='/timercontrol'
element={
<EditorFeatureWrapper>
<TimerControl />
</EditorFeatureWrapper>
}
/>
<Route
path='/messagecontrol'
element={
<EditorFeatureWrapper>
<MessageControl />
</EditorFeatureWrapper>
}
/>
<Route
path='/info'
element={
<EditorFeatureWrapper>
<Info />
</EditorFeatureWrapper>
}
/>
{/*/!* Send to default if nothing found *!/*/}
<Route path='*' element={<STimer />} />
</Routes>
);
}
@@ -0,0 +1,9 @@
import { QueryClient } from '@tanstack/react-query';
export const queryClientMock = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
+22
View File
@@ -0,0 +1,22 @@
// Exported viewer link location
const minimalLocation = 'minimal';
const speakerLocation = 'speaker';
const smLocation = 'sm';
const publicLocation = 'public';
const studioLocation = 'studio';
const cuesheetLocation = 'cuesheet';
const countdownLocation = 'countdown';
const clockLocation = 'clock';
const lowerLocation = 'lower';
export const viewerLocations = [
{ link: speakerLocation, label: 'Stage timer' },
{ link: clockLocation, label: 'Clock' },
{ link: minimalLocation, label: 'Minimal timer' },
{ link: smLocation, label: 'Backstage screen' },
{ link: publicLocation, label: 'Public screen' },
{ link: lowerLocation, label: 'Lower thirds' },
{ link: studioLocation, label: 'Studio clock' },
{ link: countdownLocation, label: 'Countdown' },
{ link: cuesheetLocation, label: 'Cuesheet' },
];
+1 -1
View File
@@ -14,7 +14,7 @@ export const RUNTIME = ['runtimeStore'];
export const serverPort = import.meta.env.DEV ? STATIC_PORT : window.location.port;
export const serverURL = import.meta.env.DEV ? `http://localhost:${serverPort}` : window.location.origin;
export const websocketUrl = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.hostname}:${serverPort}/ws`;
export const websocketUrl = `ws://${window.location.hostname}:${serverPort}/ws`;
export const eventURL = `${serverURL}/eventdata`;
export const rundownURL = `${serverURL}/events`;
+2 -18
View File
@@ -1,13 +1,5 @@
import axios from 'axios';
import {
Alias,
EventData,
OSCSettings,
OscSubscription,
Settings,
UserFields,
ViewSettings,
} from 'ontime-types';
import { Alias, EventData, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types';
import { apiRepoLatest } from '../../externals';
import { InfoType } from '../models/Info';
@@ -108,14 +100,6 @@ export async function postOSC(data: OSCSettings) {
return axios.post(`${ontimeURL}/osc`, data);
}
/**
* @description HTTP request to mutate osc subscriptions
* @return {Promise}
*/
export async function postOscSubscriptions(data: OscSubscription) {
return axios.post(`${ontimeURL}/osc-subscriptions`, data);
}
/**
* @description HTTP request to download db
* @return {Promise}
@@ -152,7 +136,7 @@ export const downloadRundown = async () => {
type UploadDataOptions = {
onlyRundown?: boolean;
};
export const uploadData = async (file: File, setProgress: (value: number) => void, options?: UploadDataOptions) => {
export const uploadData = async (file: string, setProgress: (value: number) => void, options?: UploadDataOptions) => {
const formData = new FormData();
formData.append('userFile', file);
const onlyRundown = options?.onlyRundown || 'false';
@@ -0,0 +1,26 @@
import { Button } from '@chakra-ui/react';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import PropTypes from 'prop-types';
export default function EnableBtn(props) {
const { active, text, actionHandler, size = 'xs' } = props;
return (
<Button
size={size}
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={actionHandler}
>
{text}
</Button>
);
}
EnableBtn.propTypes = {
active: PropTypes.bool,
text: PropTypes.string,
actionHandler: PropTypes.func,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
}
@@ -0,0 +1,28 @@
import { IconButton, Tooltip } from '@chakra-ui/react';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { tooltipDelayMid } from '../../../ontimeConfig';
interface PauseIconBtnProps {
clickhandler: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
active: boolean;
disabled: boolean;
}
export default function PauseIconBtn(props: PauseIconBtnProps) {
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Pause timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPause size='24px' />}
colorScheme='orange'
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
width={120}
disabled={disabled}
aria-label='Pause playback'
{...rest}
/>
</Tooltip>
);
}
@@ -0,0 +1,25 @@
import { IconButton, Tooltip } from '@chakra-ui/react';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
import PropTypes from 'prop-types';
export default function PublicIconBtn(props) {
const { actionHandler, active, size = 'xs', ...rest } = props;
return (
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
<IconButton
size={size}
icon={<FiUsers />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
{...rest}
/>
</Tooltip>
);
}
PublicIconBtn.propTypes = {
actionHandler: PropTypes.func,
active: PropTypes.bool,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
};
@@ -0,0 +1,28 @@
import { IconButton, Tooltip } from '@chakra-ui/react';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function RollIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Roll mode' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoTimeOutline size='24px' />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
width={120}
disabled={disabled}
{...rest}
/>
</Tooltip>
);
}
RollIconBtn.propTypes = {
clickhandler: PropTypes.func,
active: PropTypes.bool,
disabled: PropTypes.bool,
};
@@ -0,0 +1,28 @@
import { IconButton, Tooltip } from '@chakra-ui/react';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function StartIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Start timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlay size='24px' />}
colorScheme='green'
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
width={120}
disabled={disabled}
{...rest}
/>
</Tooltip>
);
}
StartIconBtn.propTypes = {
clickhandler: PropTypes.func,
active: PropTypes.bool,
disabled: PropTypes.bool
}
@@ -2,7 +2,7 @@ import { MouseEvent } from 'react';
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
interface TooltipActionBtnProps extends IconButtonProps {
clickHandler: (event?: MouseEvent) => void | Promise<void>;
clickHandler: (event?: MouseEvent) => void;
tooltip: string;
openDelay?: number;
}
@@ -0,0 +1,29 @@
import { IconButton, Tooltip } from '@chakra-ui/react';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function TransportIconBtn(props) {
const { clickHandler, icon, tooltip, disabled, ...rest } = props;
return (
<Tooltip label={tooltip} openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={icon}
colorScheme='white'
variant='outline'
_hover={!disabled && { bg: '#ebedf0', color: '#333' }}
onClick={clickHandler}
width={90}
disabled={disabled}
{...rest}
/>
</Tooltip>
);
}
TransportIconBtn.propTypes = {
clickHandler: PropTypes.func,
icon: PropTypes.element,
tooltip: PropTypes.string,
disabled: PropTypes.bool,
};
@@ -0,0 +1,27 @@
import { IconButton, Tooltip } from '@chakra-ui/react';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function UnloadIconBtn(props) {
const { clickHandler, disabled, ...rest } = props;
return (
<Tooltip label='Unload event' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoStop size='22px' />}
colorScheme='red'
variant='outline'
onClick={clickHandler}
width={90}
disabled={disabled}
{...rest}
/>
</Tooltip>
);
}
UnloadIconBtn.propTypes = {
clickHandler: PropTypes.func,
disabled: PropTypes.bool,
};
@@ -4,7 +4,6 @@ import { IoCopy } from '@react-icons/all-files/io5/IoCopy';
import { tooltipDelayFast } from '../../../ontimeConfig';
import { Size } from '../../models/Util.type';
import copyToClipboard from '../../utils/copyToClipboard';
interface CopyTagProps {
label: string;
@@ -15,7 +14,10 @@ interface CopyTagProps {
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
const { label, className, size = 'xs', children } = props;
const handleClick = () => copyToClipboard(children as string);
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}>
@@ -1,11 +1,7 @@
/* eslint-disable react/destructuring-assignment */
import React from 'react';
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
import * as Sentry from '@sentry/react';
import { runtime } from '@/common/stores/runtime';
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
import style from './ErrorBoundary.module.scss';
class ErrorBoundary extends React.Component {
@@ -28,9 +24,7 @@ class ErrorBoundary extends React.Component {
});
Sentry.withScope((scope) => {
scope.setExtras('error', error);
scope.setExtras('store', runtime.getState());
scope.setExtras('hasSocket', { hasConnected, shouldReconnect, reconnectAttempts });
scope.setExtras(error);
const eventId = Sentry.captureException(error);
this.setState({ eventId, info });
});
@@ -11,7 +11,6 @@ import { TimeEntryField } from '../../../utils/timesManager';
import style from './TimeInput.module.scss';
interface TimeInputProps {
id?: TimeEntryField;
name: TimeEntryField;
submitHandler: (field: TimeEntryField, value: number) => void;
time?: number;
@@ -37,7 +36,7 @@ function ButtonTooltip(name: TimeEntryField, warning?: string) {
}
export default function TimeInput(props: TimeInputProps) {
const { id, name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
const { emitError } = useEmitLog();
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState<string>('');
@@ -190,7 +189,6 @@ export default function TimeInput(props: TimeInputProps) {
</InputLeftElement>
<Input
ref={inputRef}
id={id}
data-testid='time-input'
className={style.inputField}
type='text'
@@ -3,7 +3,7 @@
$progress-bar-size: 12px;
$progress-bar-br: 3px;
.multiprogress-bar {
.progress-bar {
position: relative;
width: 100%;
height: $progress-bar-size;
@@ -17,7 +17,7 @@ $progress-bar-br: 3px;
}
}
.multiprogress-bar__indicator {
.progress-bar__indicator {
position: absolute;
height: inherit;
background-color: black;
@@ -27,7 +27,7 @@ $progress-bar-br: 3px;
right: 0;
}
.multiprogress-bar__bg-normal {
.progress-bar__bg-normal {
position: absolute;
height: inherit;
right: 0;
@@ -35,14 +35,14 @@ $progress-bar-br: 3px;
width: 100%;
}
.multiprogress-bar__bg-warning {
.progress-bar__bg-warning {
position: absolute;
height: inherit;
right: 0;
border-radius: 0 $progress-bar-br $progress-bar-br 0;
}
.multiprogress-bar__bg-danger {
.progress-bar__bg-danger {
position: absolute;
height: inherit;
right: 0;
@@ -23,11 +23,11 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
const warningWidth = clamp((warning / complete) * 100, 0, 100);
return (
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
<div className='multiprogress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
<div className='multiprogress-bar__bg-warning' style={{ width: `${warningWidth}%`, backgroundColor: warningColor }} />
<div className='multiprogress-bar__bg-danger' style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }} />
<div className='multiprogress-bar__indicator' style={{ width: `${percentComplete}%` }} />
<div className={`progress-bar ${hidden ? 'progress-bar--hidden' : ''} ${className}`}>
<div className='progress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
<div className='progress-bar__bg-warning' style={{ width: `${warningWidth}%`, backgroundColor: warningColor }} />
<div className='progress-bar__bg-danger' style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }} />
<div className='progress-bar__indicator' style={{ width: `${percentComplete}%` }} />
</div>
);
}
@@ -1,6 +1,6 @@
@use '../../../theme/v2Styles' as *;
@use '../../../theme/mixins' as *;
@use '../../../theme/ontimeColours' as *;
@use "../../../theme/v2Styles" as *;
@use "../../../theme/mixins" as *;
@use "../../../theme/ontimeColours" as *;
$menu-bg: $gray-1200;
$menu-hover-bg: $gray-1350;
@@ -14,27 +14,14 @@ $button-size: 48px;
transform: rotate(180deg);
}
.buttonContainer {
display: flex;
flex-direction: column;
row-gap: 1rem;
padding: 0.5em;
.navButton {
z-index: 2;
position: absolute;
left: 0.5em;
top: 0.5em;
transition-property: opacity;
transition-duration: 0.3s;
opacity: 1;
position: fixed;
left: 0;
top: 0;
z-index: 2;
&.hidden {
opacity: 0;
}
}
.button {
font-size: 24px;
color: $icon-color;
background-color: $button-bg;
@@ -43,11 +30,10 @@ $button-size: 48px;
display: grid;
place-content: center;
border-radius: 3px;
}
.navButton {
@extend .button;
z-index: 3;
&.hidden {
opacity: 0;
}
}
.menuContainer {
@@ -1,11 +1,10 @@
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Link, useLocation, useSearchParams } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import { IoApps } from '@react-icons/all-files/io5/IoApps';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import { IoContract } from '@react-icons/all-files/io5/IoContract';
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
import { IoPencilSharp } from '@react-icons/all-files/io5/IoPencilSharp';
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
import { navigatorConstants } from '../../../viewerConfig';
@@ -22,13 +21,12 @@ export default function NavigationMenu() {
const { isFullScreen, toggleFullScreen } = useFullscreen();
const { mirror, toggleMirror } = useViewOptionsStore();
const [showButton, setShowButton] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
useClickOutside(menuRef, () => setShowMenu(false));
const toggleMenu = () => setShowMenu((prev) => !prev);
useKeyDown(toggleMenu, ' ', { isDisabled: searchParams.get('edit') === 'true' });
useKeyDown(toggleMenu, ' ');
useEffect(() => {
let fadeOut: NodeJS.Timeout | null = null;
@@ -51,68 +49,63 @@ export default function NavigationMenu() {
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
const handleFullscreen = () => toggleFullScreen();
const handleMirror = () => toggleMirror();
const showEditFormDrawer = () => {
searchParams.append('edit', 'true');
setSearchParams(searchParams);
};
return createPortal(
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
<IoApps />
</button>
<button className={style.button} onClick={showEditFormDrawer}>
<IoPencilSharp />
</button>
{showMenu && (
<div className={style.menuContainer} data-testid='navigation-menu'>
<div className={style.buttonsContainer}>
<div
className={style.link}
tabIndex={0}
role='button'
onClick={handleFullscreen}
onKeyDown={(event) => {
isKeyEnter(event) && handleFullscreen();
}}
>
Toggle Fullscreen
{isFullScreen ? <IoContract /> : <IoExpand />}
</div>
<div
className={style.link}
tabIndex={0}
role='button'
onClick={handleMirror}
onKeyDown={(event) => {
isKeyEnter(event) && handleMirror();
}}
>
Flip Screen
<IoSwapVertical />
</div>
{/*<div className={style.link} tabIndex={0}>*/}
{/* Rename Client*/}
{/*</div>*/}
</div>
<hr className={style.separator} />
{navigatorConstants.map((route) => (
<Link
key={route.url}
to={route.url}
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
tabIndex={0}
>
{route.label}
<IoArrowUp className={style.linkIcon} />
</Link>
))}
</div>
)}
</div>
</div>,
<button
onClick={toggleMenu}
aria-label='toggle menu'
className={`${style.navButton} ${!showButton && !showMenu ? style.hidden : ''}`}
>
<IoApps />
</button>
{showMenu && (
<div className={style.menuContainer} data-testid='navigation-menu'>
<div className={style.buttonsContainer}>
<div
className={style.link}
tabIndex={0}
role='button'
onClick={handleFullscreen}
onKeyDown={(event) => {
isKeyEnter(event) && handleFullscreen();
}}
>
Toggle Fullscreen
{isFullScreen ? <IoContract /> : <IoExpand />}
</div>
<div
className={style.link}
tabIndex={0}
role='button'
onClick={handleMirror}
onKeyDown={(event) => {
isKeyEnter(event) && handleMirror();
}}
>
Flip Screen
<IoSwapVertical />
</div>
{/*<div className={style.link} tabIndex={0}>*/}
{/* Rename Client*/}
{/*</div>*/}
</div>
<hr className={style.separator} />
{navigatorConstants.map((route) => (
<Link
key={route.url}
to={route.url}
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
tabIndex={0}
>
{route.label}
<IoArrowUp className={style.linkIcon} />
</Link>
))}
</div>
)}
</div>,
document.body,
);
}
@@ -16,8 +16,8 @@ $progress-bar-br: 6px;
.progress-bar__indicator {
height: $progress-bar-size;
border-radius: $progress-bar-br;
background-color: var(--timer-progress-override, $accent-color);
transition: 1s linear;
border-radius: $progress-bar-br 0 0 $progress-bar-br;
transition-property: width;
}
@@ -10,7 +10,7 @@ interface ScheduleProps {
}
export default function Schedule({ className }: ScheduleProps) {
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
const { paginatedEvents, selectedEventId, isBackstage } = useSchedule();
if (paginatedEvents?.length < 1) {
return <Empty text='No events to show' />;
@@ -21,14 +21,10 @@ export default function Schedule({ className }: ScheduleProps) {
return (
<ul className={`schedule ${className}`}>
{paginatedEvents.map((event) => {
if (scheduleType === 'past' || scheduleType === 'future') {
selectedState = scheduleType;
} else {
if (event.id === selectedEventId) {
selectedState = 'now';
} else if (selectedState === 'now') {
selectedState = 'future';
}
if (event.id === selectedEventId) {
selectedState = 'now';
} else if (selectedState === 'now') {
selectedState = 'future';
}
return (
<ScheduleItem
@@ -7,7 +7,6 @@ interface ScheduleContextState {
events: OntimeEvent[];
paginatedEvents: OntimeEvent[];
selectedEventId: string | null;
scheduleType: 'past' | 'now' | 'future';
numPages: number;
visiblePage: number;
isBackstage: boolean;
@@ -28,7 +27,7 @@ export const ScheduleProvider = ({
events,
selectedEventId,
isBackstage = false,
eventsPerPage = 7,
eventsPerPage = 8,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
const [visiblePage, setVisiblePage] = useState(0);
@@ -37,18 +36,6 @@ export const ScheduleProvider = ({
const eventStart = eventsPerPage * visiblePage;
const eventEnd = eventsPerPage * (visiblePage + 1);
const paginatedEvents = events.slice(eventStart, eventEnd);
const selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
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(() => {
@@ -64,7 +51,6 @@ export const ScheduleProvider = ({
events,
paginatedEvents,
selectedEventId,
scheduleType,
numPages,
visiblePage,
isBackstage,
@@ -7,7 +7,7 @@
text-align: center;
letter-spacing: 0.1em;
font-weight: 600;
font-size: 3.5rem;
font-size: 3.75em;
&--finished {
color: $timer-finished-color;
@@ -1,5 +1,6 @@
import { memo } from 'react';
import { formatDisplay } from 'ontime-utils';
import { formatDisplay } from '../../utils/dateConfig';
import './TimerDisplay.scss';
@@ -0,0 +1,45 @@
@use '../../../theme/v2Styles' as *;
.modalBody {
min-height: 40vh;
display: flex;
flex-direction: column;
gap: 16px;
.options {
margin-bottom: 1.5em;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.notes {
font-size: 0.9em;
}
.info {
background-color: $bg-container-onlight;
margin: 1em 0;
padding: 0.5em;
border-radius: 2px;
color: $text-black;
position: relative;
}
.corner {
position: absolute;
right: 4px;
top: 4px;
}
.infoList {
font-size: 0.9em;
padding-left: 8px;
}
.flexColumnLeft {
display: flex;
flex-direction: column;
align-items: flex-start;
}
}
@@ -0,0 +1,137 @@
import { ChangeEvent, useCallback, useRef, useState } from 'react';
import {
Button,
Checkbox,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Progress,
} from '@chakra-ui/react';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import { useQueryClient } from '@tanstack/react-query';
import { RUNDOWN_TABLE } from '../../api/apiConstants';
import { uploadData } from '../../api/ontimeApi';
import { useEmitLog } from '../../stores/logger';
import TooltipActionBtn from '../buttons/TooltipActionBtn';
import { validateFile } from './utils';
import style from './UploadModal.module.scss';
interface UploadModalProps {
onClose: () => void;
isOpen: boolean;
}
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
const queryClient = useQueryClient();
const { emitError } = useEmitLog();
const [errors, setErrors] = useState<string[]>([]);
const [file, setFile] = useState<File | null>(null);
const [progress, setProgress] = useState(0);
const overrideOptionRef = useRef<HTMLInputElement>(null);
const handleFile = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const fileUploaded = event?.target?.files?.[0];
if (!fileUploaded) return;
const validate = validateFile(fileUploaded);
setErrors(validate.errors);
if (validate.isValid) {
setFile(fileUploaded);
} else {
setFile(null);
}
}, []);
const handleUpload = useCallback(async () => {
if (file) {
try {
await uploadData(file, setProgress, { onlyEvents: overrideOptionRef?.current?.checked });
} catch (error) {
emitError(`Failed uploading file: ${error}`);
} finally {
await queryClient.invalidateQueries(RUNDOWN_TABLE);
setFile(null);
}
}
}, [emitError, file, queryClient]);
return (
<Modal
onClose={onClose}
isOpen={isOpen}
closeOnOverlayClick={false}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>File upload</ModalHeader>
<ModalCloseButton />
<ModalBody className={style.modalBody}>
<FormControl isInvalid={errors.length > 0}>
<FormLabel>Select file to upload</FormLabel>
<Input type='file' onChange={handleFile} accept='.json, .xlsx' />
{errors.length === 0 ? (
<FormHelperText>.XLSX .JSON with max 1MB</FormHelperText>
) : (
<FormErrorMessage className={style.flexColumnLeft}>
{errors.map((error) => (
<span key={error}>{error}</span>
))}
</FormErrorMessage>
)}
</FormControl>
<div className={style.options}>
<b>Options</b>
<Checkbox ref={overrideOptionRef}>Import only events</Checkbox>
<span className={style.notes}>This will prevent overriding user settings</span>
</div>
{file && (
<div className={style.info}>
<span>File ready to upload</span>
<TooltipActionBtn
clickHandler={() => setFile(null)}
tooltip='Cancel'
aria-label='Cancel'
className={style.corner}
size='sm'
variant='ghosted'
icon={<IoCloseSharp />}
/>
<ul className={style.infoList}>
<li>{file.name}</li>
<li>{`${(file.size / 1024).toFixed(2)}kb`}</li>
<li>{file.type}</li>
</ul>
</div>
)}
<Progress value={progress} />
</ModalBody>
<ModalFooter>
<Button
colorScheme='blue'
disabled={!file || errors.length > 0}
onClick={handleUpload}
isLoading={progress < 0 && progress >= 100}
>
Upload
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
@@ -4,7 +4,7 @@ type ValidationStatus = {
};
export function validateFile(file: File): ValidationStatus {
const status: ValidationStatus = { errors: [], isValid: true };
const status:ValidationStatus = { errors: [], isValid: true };
if (!file) {
status.errors.push('No file to upload');
status.isValid = false;
@@ -1,47 +0,0 @@
import { useSearchParams } from 'react-router-dom';
import { Input, Select, Switch } from '@chakra-ui/react';
import { isStringBoolean } from '../../utils/viewUtils';
import { ParamField } from './types';
interface EditFormInputProps {
paramField: ParamField;
}
export default function ParamInput({ paramField }: EditFormInputProps) {
const [searchParams] = useSearchParams();
const { id, type } = paramField;
if (type === 'option') {
const optionFromParams = searchParams.get(id);
const defaultOptionValue = paramField.values.find((value) => value === optionFromParams);
return (
<Select placeholder='Select an option' variant='ontime' name={id} defaultValue={defaultOptionValue}>
{paramField.values.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</Select>
);
}
if (type === 'boolean') {
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) ?? false;
// checked value should be 'true', so it can be captured by the form event
return <Switch variant='ontime' name={id} defaultChecked={defaultCheckedValue} value='true' />;
}
if (type === 'number') {
const defaultNumberValue = searchParams.get(id) ?? '';
return <Input type='number' step='any' variant='ontime-filled' name={id} defaultValue={defaultNumberValue} />;
}
const defaultStringValue = searchParams.get(id) ?? '';
return <Input variant='ontime-filled' name={id} defaultValue={defaultStringValue} />;
}
@@ -1,51 +0,0 @@
@use '../../../theme/v2Styles' as *;
@use '../../../theme/ontimeColours' as *;
.drawerContent {
background-color: $gray-1200;
}
.drawerHeader {
@extend .drawerContent;
color: $section-white;
}
.drawerFooter {
@extend .drawerContent;
display: flex;
justify-content: start;
gap: $element-spacing;
button[type='reset'] {
padding: 0 2em;
margin-right: auto;
}
button[type='submit'] {
padding: 0 2em;
}
}
.label {
font-size: $inner-section-text-size;
color: $label-gray;
}
.columnSection {
display: flex;
padding: $element-spacing;
flex-direction: column;
gap: $element-inner-spacing;
}
.title {
font-size: $inner-section-text-size;
display: block;
width: 100%;
}
.description {
font-size: $inner-section-text-size;
display: block;
color: $modal-note-color;
}
@@ -1,101 +0,0 @@
import { FormEvent, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
Button,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
useDisclosure,
} from '@chakra-ui/react';
import ParamInput from './ParamInput';
import { ParamField } from './types';
import style from './ViewParamsEditor.module.scss';
interface EditFormDrawerProps {
paramFields: ParamField[];
}
export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
const [searchParams, setSearchParams] = useSearchParams();
const { isOpen, onClose, onOpen } = useDisclosure();
useEffect(() => {
const isEditing = searchParams.get('edit');
if (isEditing === 'true') {
return onOpen();
}
}, [searchParams, onOpen]);
const onEditDrawerClose = () => {
onClose();
searchParams.delete('edit');
setSearchParams(searchParams);
};
const clearParams = () => {
setSearchParams();
onClose();
};
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
formEvent.preventDefault();
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
const newSearchParams = Object.entries(newParamsObject).reduce((newSearchParams, [id, value]) => {
if (typeof value === 'string' && value.length) {
newSearchParams.set(id, value);
return newSearchParams;
}
return newSearchParams;
}, new URLSearchParams());
setSearchParams(newSearchParams);
};
return (
<Drawer isOpen={isOpen} placement='right' onClose={onEditDrawerClose} size='lg'>
<DrawerOverlay />
<DrawerContent>
<DrawerHeader className={style.drawerHeader}>
<DrawerCloseButton _hover={{ bg: '#ebedf0', color: '#333' }} size='lg' />
Customise
</DrawerHeader>
<DrawerBody className={style.drawerContent}>
<form id='edit-params-form' onSubmit={onParamsFormSubmit}>
{paramFields.map((field) => (
<div key={field.title} className={style.columnSection}>
<label className={style.label}>
<span className={style.title}>{field.title}</span>
<span className={style.description}>{field.description}</span>
<ParamInput key={field.title} paramField={field} />
</label>
</div>
))}
</form>
</DrawerBody>
<DrawerFooter className={style.drawerFooter}>
<Button variant='ontime-ghosted' onClick={clearParams} type='reset'>
Clear
</Button>
<Button variant='ontime-subtle' onClick={onEditDrawerClose}>
Cancel
</Button>
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
Save
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
@@ -1,203 +0,0 @@
import { ParamField } from './types';
export const TIME_FORMAT_OPTION: ParamField = {
id: 'format',
title: '12 / 24 hour timer',
description: 'Whether to show the time in 12 or 24 hour mode. Overrides the global setting from preferences',
type: 'option',
values: ['12', '24'],
};
export const CLOCK_OPTIONS: ParamField[] = [
TIME_FORMAT_OPTION,
{
id: 'key',
title: 'Key Colour',
description: 'Background colour in hexadecimal',
type: 'string',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour in hexadecimal',
type: 'string',
},
{
id: 'textbg',
title: 'Text Background',
description: 'Colour of text background in hexadecimal',
type: 'string',
},
{
id: 'font',
title: 'Font',
description: 'Font family, will use the fonts available in the system',
type: 'string',
},
{
id: 'size',
title: 'Text Size',
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
type: 'number',
},
{
id: 'alignx',
title: 'Align Horizontal',
description: 'Moves the horizontally in page to start = left | center | end = right',
type: 'option',
values: ['start', 'center', 'end'],
},
{
id: 'offsetx',
title: 'Offset Horizontal',
description: 'Offsets the timer horizontal position by a given amount in pixels',
type: 'number',
},
{
id: 'aligny',
title: 'Align Vertical',
description: 'Moves the vertically in page to start = left | center | end = right',
type: 'option',
values: ['start', 'center', 'end'],
},
{
id: 'offsety',
title: 'Offset Vertical',
description: 'Offsets the timer vertical position by a given amount in pixels',
type: 'number',
},
];
export const TIMER_OPTIONS: ParamField[] = [TIME_FORMAT_OPTION];
export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
{
id: 'key',
title: 'Key Colour',
description: 'Background colour in hexadecimal',
type: 'string',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour in hexadecimal',
type: 'string',
},
{
id: 'textbg',
title: 'Text Background',
description: 'Colour of text background in hexadecimal',
type: 'string',
},
{
id: 'font',
title: 'Font',
description: 'Font family, will use the fonts available in the system',
type: 'string',
},
{
id: 'size',
title: 'Text Size',
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
type: 'number',
},
{
id: 'alignx',
title: 'Align Horizontal',
description: 'Moves the horizontally in page to start = left | center | end = right',
type: 'option',
values: ['start', 'center', 'end'],
},
{
id: 'offsetx',
title: 'Offset Horizontal',
description: 'Offsets the timer horizontal position by a given amount in pixels',
type: 'number',
},
{
id: 'aligny',
title: 'Align Vertical',
description: 'Moves the vertically in page to start = left | center | end = right',
type: 'option',
values: ['start', 'center', 'end'],
},
{
id: 'offsety',
title: 'Offset Vertical',
description: 'Offsets the timer vertical position by a given amount in pixels',
type: 'number',
},
{
id: 'hideovertime',
title: 'Hide Overtime',
description: 'Whether to supress overtime styles (red borders and red text)',
type: 'boolean',
},
{
id: 'hidemessages',
title: 'Hide Message Overlay',
description: 'Whether to hide the overlay from showing timer screen messages',
type: 'boolean',
},
{
id: 'hideendmessage',
title: 'Hide End Message',
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
type: 'boolean',
},
];
export const LOWER_THIRDS_OPTIONS: ParamField[] = [
{
id: 'preset',
title: 'Preset',
description: 'Selects a style preset (0-1)',
type: 'number',
},
{
id: 'size',
title: 'Size',
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
type: 'number',
},
{
id: 'transition',
title: 'Transition',
description: 'Transition in time in seconds (default 5)',
type: 'number',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour in hexadecimal',
type: 'string',
},
{
id: 'bg',
title: 'Text Background',
description: 'Text background colour in hexadecimal',
type: 'string',
},
{
id: 'key',
title: 'Key Colour',
description: 'Screen background colour in hexadecimal',
type: 'string',
},
{
id: 'fadeout',
title: 'Fadeout',
description: 'Time (in seconds) the lower third displays before fading out',
type: 'number',
},
];
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
TIME_FORMAT_OPTION,
{
id: 'seconds',
title: 'Show Seconds',
description: 'Shows seconds in clock',
type: 'boolean',
},
];
@@ -1,12 +0,0 @@
type BaseField = {
id: string;
title: string;
description: string;
};
type OptionsField = { type: 'option'; values: string[] };
type StringField = { type: 'string' };
type BooleanField = { type: 'boolean' };
type NumberField = { type: 'number' };
export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField);
@@ -1,9 +0,0 @@
.contextMenuButton {
position: absolute;
cursor: pointer;
}
.contextMenuBackdrop {
position: fixed;
inset: 0;
}
@@ -1,76 +0,0 @@
// logic (with some modifications) culled from:
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
import { createContext, ReactNode, useState } from 'react';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
import { IconType } from '@react-icons/all-files';
import style from './ContextMenuContext.module.scss';
type ContextMenuCoords = {
x: number;
y: number;
};
type ContextMenuContextType = {
createContextMenu: (options: Option[], menuCoordinates: ContextMenuCoords) => void;
};
export const ContextMenuContext = createContext<ContextMenuContextType | null>(null);
export type Option = {
label: string;
icon: IconType;
onClick: () => void;
};
interface ContextMenuProviderProps {
children: ReactNode;
}
export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
const [isOpen, setIsOpen] = useState(false);
const [coords, setCoords] = useState<ContextMenuCoords>({ x: 0, y: 0 });
const [options, setOptions] = useState<Option[]>([]);
const onClose = () => {
return setIsOpen(false);
};
const createContextMenu = (options: Option[], menuCoords: ContextMenuCoords) => {
setCoords(menuCoords);
setOptions(options);
setIsOpen(true);
};
return (
<ContextMenuContext.Provider value={{ createContextMenu }}>
{children}
{isOpen && (
<>
<div className={style.contextMenuBackdrop} />
<Menu isOpen gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<MenuButton
className={style.contextMenuButton}
aria-hidden
w={1}
h={1}
style={{
left: coords.x,
top: coords.y,
}}
/>
<MenuList>
{options.map(({ label, icon: Icon, onClick }, i) => (
<MenuItem key={i} icon={<Icon />} onClick={onClick}>
{label}
</MenuItem>
))}
</MenuList>
</Menu>
</>
)}
</ContextMenuContext.Provider>
);
};
@@ -5,7 +5,7 @@ import { ALIASES } from '../api/apiConstants';
import { getAliases } from '../api/ontimeApi';
export default function useAliases() {
const { data, status, isFetching, isError, refetch } = useQuery({
const { data, status, isError, refetch } = useQuery({
queryKey: ALIASES,
queryFn: getAliases,
placeholderData: [],
@@ -15,5 +15,5 @@ export default function useAliases() {
networkMode: 'always',
});
return { data, status, isFetching, isError, refetch };
}
return { data, status, isError, refetch };
}
@@ -6,7 +6,7 @@ import { fetchEventData } from '../api/eventDataApi';
import { eventDataPlaceholder } from '../models/EventData';
export default function useEventData() {
const { data, status, isFetching, isError, refetch } = useQuery({
const { data, status, isError, refetch } = useQuery({
queryKey: EVENT_DATA,
queryFn: fetchEventData,
placeholderData: eventDataPlaceholder,
@@ -16,5 +16,5 @@ export default function useEventData() {
networkMode: 'always',
});
return { data, status, isFetching, isError, refetch };
return { data, status, isError, refetch };
}
@@ -3,12 +3,12 @@ import { OSCSettings } from 'ontime-types';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { OSC_SETTINGS } from '../api/apiConstants';
import { getOSC, postOSC, postOscSubscriptions } from '../api/ontimeApi';
import { getOSC, postOSC } from '../api/ontimeApi';
import { oscPlaceholderSettings } from '../models/OscSettings';
import { ontimeQueryClient } from '../queryClient';
export default function useOscSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
const { data, status, isError, refetch } = useQuery({
queryKey: OSC_SETTINGS,
queryFn: getOSC,
placeholderData: oscPlaceholderSettings,
@@ -18,8 +18,7 @@ export default function useOscSettings() {
networkMode: 'always',
});
// we need to jump through some hoops because of the type op port
return { data: data! as unknown as OSCSettings, status, isFetching, isError, refetch };
return { data: data! as unknown as OSCSettings, status, isError, refetch };
}
export function useOscSettingsMutation() {
@@ -30,11 +29,3 @@ export function useOscSettingsMutation() {
});
return { isLoading, mutateAsync };
}
export function usePostOscSubscriptions() {
const { isLoading, mutateAsync } = useMutation({
mutationFn: postOscSubscriptions,
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
});
return { isLoading, mutateAsync };
}
@@ -6,7 +6,7 @@ import { getSettings } from '../api/ontimeApi';
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
export default function useSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
const { data, status, isError, refetch } = useQuery({
queryKey: APP_SETTINGS,
queryFn: getSettings,
placeholderData: ontimePlaceholderSettings,
@@ -16,5 +16,5 @@ export default function useSettings() {
networkMode: 'always',
});
return { data, status, isFetching, isError, refetch };
return { data, status, isError, refetch };
}
@@ -6,7 +6,7 @@ import { getUserFields } from '../api/ontimeApi';
import { userFieldsPlaceholder } from '../models/UserFields';
export default function useUserFields() {
const { data, status, isFetching, isError, refetch } = useQuery({
const { data, status, isError, refetch } = useQuery({
queryKey: USERFIELDS,
queryFn: getUserFields,
placeholderData: userFieldsPlaceholder,
@@ -16,5 +16,5 @@ export default function useUserFields() {
networkMode: 'always',
});
return { data, status, isFetching, isError, refetch };
return { data, status, isError, refetch };
}
@@ -6,7 +6,7 @@ import { getView } from '../api/ontimeApi';
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
export default function useViewSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
const { data, status, isError, refetch } = useQuery({
queryKey: VIEW_SETTINGS,
queryFn: getView,
placeholderData: viewsSettingsPlaceholder,
@@ -16,5 +16,5 @@ export default function useViewSettings() {
networkMode: 'always',
});
return { data, status, isError, refetch, isFetching };
return { data, status, isError, refetch };
}
@@ -1,23 +0,0 @@
import { MouseEvent, useContext } from 'react';
import { ContextMenuContext, Option } from '../context/ContextMenuContext';
export const useContextMenu = <T extends HTMLElement>(options: Option[]) => {
const contextMenuContext = useContext(ContextMenuContext);
if (contextMenuContext === null) {
throw new Error('useContextMenu should be wrapped by ContextMenuProvider');
}
const { createContextMenu } = contextMenuContext;
const localCreateContextMenu = (contextMenuEvent: MouseEvent<T, globalThis.MouseEvent>) => {
// prevent browser default context menu from showing up
contextMenuEvent.preventDefault();
const { pageX, pageY } = contextMenuEvent;
return createContextMenu(options, { x: pageX, y: pageY });
};
return [localCreateContextMenu];
};
@@ -0,0 +1,43 @@
import { useCallback, useEffect, useState } from 'react';
export default function useFullscreen() {
const [isFullScreen, setFullScreen] = useState(document.fullscreenElement);
useEffect(() => {
const handleChange = () => {
setFullScreen(document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleChange, { passive: true });
document.addEventListener('resize', handleChange, { passive: true });
return () => {
document.removeEventListener('fullscreenchange', handleChange, { passive: true });
document.removeEventListener('resize', handleChange, { passive: true });
};
}, []);
const toggleFullScreen = useCallback(() => {
if (!document.fullscreenElement && !document.webkitIsFullScreen) {
// Fullscreen mode is not active, so we can enter fullscreen mode
if (document.documentElement.requestFullscreen) {
// Standard fullscreen API is supported
document.documentElement.requestFullscreen();
} else if (document.documentElement.webkitRequestFullscreen) {
// iOS Safari fullscreen API is supported
document.documentElement.webkitRequestFullscreen();
}
} else {
// Fullscreen mode is active, so we can exit fullscreen mode
if (document.exitFullscreen) {
// Standard fullscreen API is supported
document.exitFullscreen();
} else if (document.webkitCancelFullscreen) {
// iOS Safari fullscreen API is supported
document.webkitCancelFullscreen();
}
}
}, []);
return { isFullScreen, toggleFullScreen };
}
@@ -1,60 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
interface WebkitDocument extends Document {
webkitFullscreenElement?: Element | null;
webkitIsFullScreen?: boolean;
webkitExitFullscreen?: () => Promise<void>;
webkitRequestFullscreen?: () => Promise<void>;
}
export default function useFullscreen() {
const [isFullScreen, setFullScreen] = useState(
document.fullscreenElement || (document as WebkitDocument).webkitFullscreenElement,
);
useEffect(() => {
const handleChange = () => {
if (typeof (document as WebkitDocument).webkitFullscreenElement !== 'undefined') {
setFullScreen((document as WebkitDocument).webkitFullscreenElement);
} else {
setFullScreen(document.fullscreenElement);
}
};
(document as WebkitDocument).addEventListener('webkitfullscreenchange', handleChange, { passive: true });
document.addEventListener('fullscreenchange', handleChange, { passive: true });
document.addEventListener('resize', handleChange, { passive: true });
return () => {
(document as WebkitDocument).removeEventListener('webkitfullscreenchange', handleChange);
document.removeEventListener('fullscreenchange', handleChange);
document.removeEventListener('resize', handleChange);
};
}, []);
const toggleFullScreen = useCallback(() => {
if (!document.fullscreenElement && !(document as WebkitDocument).webkitIsFullScreen) {
// Fullscreen mode is not active, so we can enter fullscreen mode
if (document.documentElement.requestFullscreen) {
// Standard fullscreen API is supported
document.documentElement.requestFullscreen();
// @ts-expect-error -- the whole casting dance is not worth it
} else if (document.documentElement.webkitRequestFullscreen) {
// iOS Safari fullscreen API is supported
// @ts-expect-error -- we know this is not undefined now
(document.documentElement as WebkitDocument).webkitRequestFullscreen();
}
} else {
// Fullscreen mode is active, so we can exit fullscreen mode
if (document.exitFullscreen) {
// Standard fullscreen API is supported
document.exitFullscreen();
} else if ((document as WebkitDocument).webkitExitFullscreen) {
// iOS Safari fullscreen API is supported
// @ts-expect-error -- we know this is not undefined now
(document as WebkitDocument).webkitExitFullscreen();
}
}
}, []);
return { isFullScreen, toggleFullScreen };
}
+10 -17
View File
@@ -1,25 +1,18 @@
import { useCallback, useEffect } from 'react';
import { useEffect } from 'react';
type UseKeyDown = (callback: () => void, targetKey: string, options?: { isDisabled?: boolean }) => void;
export const useKeyDown: UseKeyDown = (callback, targetKey, options = {}) => {
const { isDisabled = false } = options;
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
const targetKeyPressed = event.key === targetKey && !event.repeat;
if (targetKeyPressed && !isDisabled) {
event.preventDefault();
callback();
}
},
[callback, isDisabled, targetKey],
);
export const useKeyDown = (callback: () => void, targetKey: string) => {
const onKeyDown = (event: KeyboardEvent) => {
const targetKeyPressed = event.key === targetKey && !event.repeat;
if (targetKeyPressed) {
event.preventDefault();
callback();
}
};
useEffect(() => {
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('keydown', onKeyDown);
};
}, [onKeyDown]);
}, []);
};
@@ -32,8 +32,6 @@ export const setMessage = {
lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload),
lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload),
onAir: (payload: boolean) => socketSendJson('set-onAir', payload),
timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', payload),
timerBlackout: (payload: boolean) => socketSendJson('set-timer-blackout', payload),
};
export const usePlaybackControl = () => {
+7
View File
@@ -0,0 +1,7 @@
import { Alias } from 'ontime-types';
export const aliasPlaceholder: Alias = {
enabled: false,
alias: '',
pathAndParams: '',
};
@@ -0,0 +1,30 @@
export const ontimeVars = [
{
name: '$timer',
description: 'Current running timer',
},
{
name: '$title',
description: 'Current title',
},
{
name: '$presenter',
description: 'Current timer',
},
{
name: '$subtitle',
description: 'Current subtitle',
},
{
name: '$next-title',
description: 'Next title',
},
{
name: '$next-presenter',
description: 'Next timer',
},
{
name: '$next-subtitle',
description: 'Next subtitle',
},
];
-2
View File
@@ -22,8 +22,6 @@ export const runtimeStorePlaceholder = {
timerMessage: {
text: '',
visible: false,
timerBlink: false,
timerBlackout: false,
},
publicMessage: {
text: '',
@@ -1,4 +1,100 @@
import { forgivingStringToMillis, millisToDelayString, millisToMinutes, millisToSeconds } from '../dateConfig';
import {
forgivingStringToMillis,
formatDisplay,
isTimeString,
millisToDelayString,
millisToMinutes,
millisToSeconds,
} from '../dateConfig';
describe('test string from formatDisplay function', () => {
it('test with null values', () => {
const t = { val: null, result: '00:00:00' };
expect(formatDisplay(t.val, false)).toBe(t.result);
});
it('test with not numbers', () => {
const t = { val: 'test', result: '00:00:00' };
expect(formatDisplay(t.val, false)).toBe(t.result);
});
it('test with valid millis', () => {
const t = { val: 3600000, result: '01:00:00' };
expect(formatDisplay(t.val, false)).toBe(t.result);
});
it('test with negative millis', () => {
const t = { val: -3600000, result: '01:00:00' };
expect(formatDisplay(t.val, false)).toBe(t.result);
});
it('test with 0', () => {
const t = { val: 0, result: '00:00:00' };
expect(formatDisplay(t.val, false)).toBe(t.result);
});
it('test with -0', () => {
const t = { val: -0, result: '00:00:00' };
expect(formatDisplay(t.val, false)).toBe(t.result);
});
it('test with 86400 (24 hours)', () => {
const t = { val: 86400000, result: '00:00:00' };
expect(formatDisplay(t.val, false)).toBe(t.result);
});
it('test with 86401 (24 hours and 1 second)', () => {
const t = { val: 86401000, result: '00:00:01' };
expect(formatDisplay(t.val, false)).toBe(t.result);
});
it('test with -86401 (-24 hours and 1 second)', () => {
const t = { val: -86401000, result: '00:00:01' };
expect(formatDisplay(t.val, false)).toBe(t.result);
});
});
describe('test string from formatDisplay function with hidezero', () => {
it('test with null values', () => {
const t = { val: null, result: '00:00' };
expect(formatDisplay(t.val, true)).toBe(t.result);
});
it('test with valid millis', () => {
const t = { val: 3600000, result: '01:00:00' };
expect(formatDisplay(t.val, true)).toBe(t.result);
});
it('test with negative millis', () => {
const t = { val: -3600000, result: '01:00:00' };
expect(formatDisplay(t.val, true)).toBe(t.result);
});
it('test with 0', () => {
const t = { val: 0, result: '00:00' };
expect(formatDisplay(t.val, true)).toBe(t.result);
});
it('test with -0', () => {
const t = { val: -0, result: '00:00' };
expect(formatDisplay(t.val, true)).toBe(t.result);
});
it('test with 86400 (24 hours)', () => {
const t = { val: 86400000, result: '00:00' };
expect(formatDisplay(t.val, true)).toBe(t.result);
});
it('test with 86401 (24 hours and 1 second)', () => {
const t = { val: 86401000, result: '00:01' };
expect(formatDisplay(t.val, true)).toBe(t.result);
});
it('test with -86401 (-24 hours and 1 second)', () => {
const t = { val: -86401000, result: '00:01' };
expect(formatDisplay(t.val, true)).toBe(t.result);
});
});
describe('test millisToSeconds function', () => {
it('test with null values', () => {
@@ -74,6 +170,31 @@ describe('test millisToMinutes function', () => {
});
});
describe('test isTimeString() function', () => {
it('it validates time strings', () => {
const ts = ['2', '2:10', '2:10:22'];
for (const s of ts) {
expect(isTimeString(s)).toBe(true);
}
});
it('it fails overloaded times', () => {
const ts = ['70', '89:10', '26:10:22'];
for (const s of ts) {
expect(isTimeString(s)).toBe(false);
}
});
});
describe('test isTimeString() function handle different separators', () => {
const ts = ['2:10', '2,10', '2.10'];
for (const s of ts) {
it(`it handles ${s}`, () => {
expect(isTimeString(s)).toBe(true);
});
}
});
describe('test forgivingStringToMillis()', () => {
describe('function handles time with no separators', () => {
const testData = [
@@ -1,4 +0,0 @@
// we need to this as a promise because safari
export default async function copyToClipboard(text: string) {
setTimeout(async () => await navigator.clipboard.writeText(text));
}
@@ -5,6 +5,29 @@ import { mth, mtm, mts } from './timeConstants';
export const timeFormat = 'HH:mm';
export const timeFormatSeconds = 'HH:mm:ss';
/**
* another go at simpler string formatting (counters)
* @description Converts seconds to string representing time
* @param {number | null} milliseconds - time in seconds
* @param {boolean} [hideZero] - whether to show hours in case its 00
* @returns {string} String representing absolute time 00:12:02
*/
export function formatDisplay(milliseconds: number | null, hideZero = false): string {
if (typeof milliseconds !== 'number') {
return hideZero ? '00:00' : '00:00:00';
}
// add an extra 0 if necessary
const format = (val: number) => `0${Math.floor(val)}`.slice(-2);
const s = Math.abs(millisToSeconds(milliseconds));
const hours = Math.floor((s / 3600) % 24);
const minutes = Math.floor((s % 3600) / 60);
if (hideZero && hours < 1) return [minutes, s % 60].map(format).join(':');
return [hours, minutes, s % 60].map(format).join(':');
}
/**
* @description Converts milliseconds to seconds
* @param {number | null} millis - time in seconds
@@ -26,6 +49,26 @@ export const millisToMinutes = (millis: number): number => {
return millis < 0 ? Math.ceil(millis / mtm) : Math.floor(millis / mtm);
};
/**
* @description Validates a time string
* @param {string} string - time string "23:00:12"
* @returns {boolean} string represents time
*/
export const isTimeString = (string: string): boolean => {
// ^ # Start of string
// (?: # Try to match...
// (?: # Try to match...
// ([01]?\d|2[0-3]): # HH:
// )? # (optionally).
// ([0-5]?\d): # MM: (required)
// )? # (entire group optional, so either HH:MM:, MM: or nothing)
// ([0-5]?\d) # SS (required)
// $ # End of string
const regex = /^(?:(?:([01]?\d|2[0-3])[:,.])?([0-5]?\d)[:,.])?([0-5]?\d)$/;
return regex.test(string);
};
/**
* @description safe parse string to int
* @param {string} valueAsString
+2 -6
View File
@@ -8,16 +8,13 @@ import { runtime } from '../stores/runtime';
export let websocket: WebSocket | null = null;
let reconnectTimeout: NodeJS.Timeout | null = null;
const reconnectInterval = 1000;
export let shouldReconnect = true;
export let hasConnected = false;
export let reconnectAttempts = 0;
let shouldReconnect = true;
export const connectSocket = () => {
websocket = new WebSocket(websocketUrl);
websocket.onopen = () => {
clearTimeout(reconnectTimeout as NodeJS.Timeout);
hasConnected = true;
reconnectAttempts = 0;
};
websocket.onclose = () => {
@@ -26,7 +23,6 @@ export const connectSocket = () => {
reconnectTimeout = setTimeout(() => {
console.warn('WebSocket: attempting reconnect');
if (websocket && websocket.readyState === WebSocket.CLOSED) {
reconnectAttempts += 1;
connectSocket();
}
}, reconnectInterval);
@@ -3,7 +3,6 @@ import { IoEye } from '@react-icons/all-files/io5/IoEye';
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { cx } from '../../../common/utils/styleUtils';
import { tooltipDelayMid } from '../../../ontimeConfig';
import style from './InputRow.module.scss';
@@ -15,19 +14,17 @@ interface InputRowProps {
visible?: boolean;
actionHandler: (action: string, payload: object) => void;
changeHandler: (newValue: string) => void;
className?: string;
}
export default function InputRow(props: InputRowProps) {
const { label, placeholder, text, visible, actionHandler, changeHandler, className } = props;
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
const handleInputChange = (newValue: string) => {
changeHandler(newValue);
};
const classes = cx([style.inputRow, className]);
return (
<div className={classes}>
<div className={style.inputRow}>
<label className={`${style.label} ${visible ? style.active : ''}`}>{label}</label>
<div className={style.inputItems}>
<Input
@@ -42,7 +39,7 @@ export default function InputRow(props: InputRowProps) {
tooltip={visible ? 'Make invisible' : 'Make visible'}
aria-label={`Toggle ${label}`}
openDelay={tooltipDelayMid}
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
icon={visible? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
size='sm'
/>
@@ -7,18 +7,12 @@
}
.onAirSection {
margin-top: $section-spacing;
display: flex;
flex-direction: column;
gap: $element-spacing;
}
.buttonSection {
display: grid;
grid-template-columns: 1fr 1fr;
gap: $element-spacing;
margin-top: -0.5rem;
}
.label {
font-size: $inner-section-text-size;
color: $label-gray;
@@ -27,7 +21,3 @@
color: $action-text-color;
}
}
.padTop {
margin-top: $section-spacing;
}
@@ -1,10 +1,6 @@
import { Button } from '@chakra-ui/react';
import { IoEye } from '@react-icons/all-files/io5/IoEye';
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline';
import { setMessage, useMessageControl } from '../../../common/hooks/useSocket';
@@ -17,6 +13,14 @@ export default function MessageControl() {
return (
<div className={style.messageContainer}>
<InputRow
label='Timer screen message'
placeholder='Shown in stage timer'
text={data.timerMessage.text || ''}
visible={data.timerMessage.visible || false}
changeHandler={(newValue) => setMessage.presenterText(newValue)}
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
/>
<InputRow
label='Public / Backstage screen message'
placeholder='Shown in public and backstage screens'
@@ -33,33 +37,7 @@ export default function MessageControl() {
changeHandler={(newValue) => setMessage.lowerText(newValue)}
actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)}
/>
<InputRow
label='Timer message'
placeholder='Shown in stage timer'
text={data.timerMessage.text || ''}
visible={data.timerMessage.visible || false}
changeHandler={(newValue) => setMessage.presenterText(newValue)}
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
/>
<div className={style.buttonSection}>
<label className={style.label}>Timer messsage blink</label>
<label className={style.label}>Blackout timer screens</label>
<Button
className={`${data.timerMessage.timerBlink ? style.blink : ''}`}
variant={data.timerMessage.timerBlink ? 'ontime-filled' : 'ontime-subtle'}
leftIcon={data.timerMessage.timerBlink ? <IoSunny size='24px' /> : <IoSunnyOutline size='24px' />}
onClick={() => setMessage.timerBlink(!data.timerMessage.timerBlink)}
data-testid='toggle timer blink'
/>
<Button
className={style.blackoutButton}
variant={data.timerMessage.timerBlackout ? 'ontime-filled' : 'ontime-subtle'}
leftIcon={data.timerMessage.timerBlackout ? <IoEye size='24px' /> : <IoEyeOffOutline size='24px' />}
onClick={() => setMessage.timerBlackout(!data.timerMessage.timerBlackout)}
data-testid='toggle timer blackout'
/>
</div>
<div className={`${style.onAirSection} ${style.padTop}`}>
<div className={style.onAirSection}>
<label className={style.label}>Toggle On Air state</label>
<Button
variant={data.onAir ? 'ontime-filled' : 'ontime-subtle'}
@@ -1,4 +1,5 @@
import { memo } from 'react';
import { Box } from '@chakra-ui/react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
@@ -10,14 +11,14 @@ import style from '../../editors/Editor.module.scss';
const MessageControlExport = () => {
return (
<div className={style.messages} data-testid='panel-messages-control'>
<Box className={style.messages} data-testid='panel-messages-control'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} />
<div className={style.content}>
<ErrorBoundary>
<MessageControl />
</ErrorBoundary>
</div>
</div>
</Box>
);
};
@@ -1,4 +1,5 @@
import { memo } from 'react';
import { Box } from '@chakra-ui/react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
@@ -10,14 +11,14 @@ import style from '../../editors/Editor.module.scss';
const TimerControlExport = () => {
return (
<div className={style.playback} data-testid='panel-timer-control'>
<Box className={style.playback} data-testid='panel-timer-control'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} />
<div className={style.content}>
<ErrorBoundary>
<PlaybackControl />
</ErrorBoundary>
</div>
</div>
</Box>
);
};
@@ -7,13 +7,13 @@
"go playback"
"go transport"
"extra extra";
grid-template-rows: repeat(3, 2rem);
grid-template-rows: repeat(3, 32px);
gap: $element-spacing;
}
.go {
grid-area: go;
font-size: 5rem;
font-size: 5em;
}
@mixin spaced-flex {
@@ -5,14 +5,14 @@ import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { IoTime } from '@react-icons/all-files/io5/IoTime';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { Playback } from 'ontime-types';
import { validatePlayback } from 'ontime-utils';
import { setPlayback } from '../../../../common/hooks/useSocket';
import { tooltipDelayMid } from '../../../../ontimeConfig';
import TapButton from '../tap-button/TapButton';
import styles from './PlaybackButtons.module.scss';
import style from './PlaybackButtons.module.scss';
interface PlaybackButtonsProps {
@@ -35,13 +35,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
const noEvents = numEvents === 0;
const disableGo = isRolling || noEvents || (isLast && !isArmed);
const disablePrev = noEvents || isFirst;
const playbackCan = validatePlayback(playback);
const disableStart = !playbackCan.start;
const disablePause = !playbackCan.pause;
const disableRoll = !playbackCan.roll || noEvents;
const disableStop = !playbackCan.stop;
const disablePrev = isRolling || noEvents || isFirst;
const goModeText = selectedEventIndex === null || isArmed ? 'Start' : 'Next';
const goModeAction = () => {
@@ -53,16 +47,26 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
};
return (
<div className={style.buttonContainer}>
<TapButton disabled={disableGo} onClick={goModeAction} aspect='fill' className={style.go}>
<div className={styles.buttonContainer}>
<TapButton disabled={disableGo} onClick={goModeAction} aspect='fill' className={styles.go}>
{goModeText}
</TapButton>
<div className={style.playbackContainer}>
<TapButton onClick={setPlayback.start} disabled={disableStart} theme={Playback.Play} active={isPlaying}>
<TapButton
onClick={setPlayback.start}
disabled={isStopped || isRolling}
theme={Playback.Play}
active={isPlaying}
>
<IoPlay />
</TapButton>
<TapButton onClick={setPlayback.pause} disabled={disablePause} theme={Playback.Pause} active={isPaused}>
<TapButton
onClick={setPlayback.pause}
disabled={isStopped || isRolling || isArmed}
theme={Playback.Pause}
active={isPaused}
>
<IoPause />
</TapButton>
</div>
@@ -78,17 +82,22 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
</TapButton>
</Tooltip>
</div>
<div className={style.extra}>
<TapButton onClick={setPlayback.roll} disabled={disableRoll} theme={Playback.Roll} active={isRolling}>
<IoTime />
<div className={styles.extra}>
<TapButton
onClick={setPlayback.roll}
disabled={!isStopped || noEvents}
theme={Playback.Roll}
active={isRolling}
>
<IoTimeOutline />
</TapButton>
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
<TapButton onClick={setPlayback.reload} disabled={isStopped}>
<TapButton onClick={setPlayback.reload} disabled={isStopped || isRolling}>
<IoReload className={style.invertX} />
</TapButton>
</Tooltip>
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
<TapButton onClick={setPlayback.stop} disabled={disableStop} theme={Playback.Stop}>
<TapButton onClick={setPlayback.stop} disabled={isStopped && !isRolling} theme={Playback.Stop}>
<IoStop />
</TapButton>
</Tooltip>
@@ -7,7 +7,7 @@
'ind clk clk btn'
'... sta fin btn';
grid-template-rows: 1fr auto;
grid-template-columns: 1.25rem 1fr 1fr 5rem;
grid-template-columns: 1.5em 1fr 1fr 5em;
gap: $element-inner-spacing;
justify-items: start;
}
@@ -15,7 +15,7 @@
.timer {
grid-area: clk;
white-space: nowrap;
max-width: 18.75rem;
max-width: 300px;
}
.indicators {
@@ -51,7 +51,7 @@
.indNegativeActive {
margin: 0 auto;
width: 90%;
height: 0.25rem;
height: 4px;
}
.indNegativeActive {
@@ -80,7 +80,7 @@
.start,
.finish,
.roll {
height: 1.5rem;
height: 24px;
}
.start {
@@ -6,7 +6,7 @@ $button-color-white: $gray-50;
@mixin tap-factory($theme-color) {
font-family: $ontime-font-family;
font-size: calc(1rem + 2px);
font-size: 18px;
border-radius: $component-border-radius-md;
width: 100%;
transition-property: color, background-color;
@@ -77,13 +77,17 @@ $button-color-white: $gray-50;
@include tap-factory($ontime-stop);
}
.tapButton.normal {
aspect-ratio: 3/1;
}
.tapButton.square {
aspect-ratio: 1;
font-size: calc(1rem - 2px);
font-size: 14px;
}
.tapButton.fill {
aspect-ratio: unset;
height: 100%;
font-size: 1.5rem;
font-size: 1.5em;
}
@@ -1,9 +1,9 @@
@use '../../theme/ontimeColours' as *;
@use '../../theme/v2Styles' as *;
$menu-width: 2.75rem;
$rundown-width: 44.5rem;
$playback-width: 26rem;
$menu-width: 48px;
$rundown-width: 46em;
$playback-width: 450px;
@mixin absolute-top-right($distance) {
position: absolute;
@@ -20,7 +20,7 @@ $playback-width: 26rem;
.corner {
display: none;
@include absolute-top-right(0.5rem);
@include absolute-top-right(8px);
transform: rotate(45deg);
}
@@ -30,7 +30,7 @@ $playback-width: 26rem;
height: 100%;
margin: auto;
color: $ui-white;
padding: 1rem 0.5rem;
padding: 16px 8px;
font-family: $ontime-font-family;
display: grid;
@@ -39,7 +39,7 @@ $playback-width: 26rem;
grid-template-areas:
'sett rundown play info'
'sett rundown mess info';
gap: 0.5rem;
gap: 8px;
.rundown,
.playback,
@@ -59,7 +59,7 @@ $playback-width: 26rem;
.mainContainer {
height: 100%;
grid-template-rows: auto 1fr;
grid-template-columns: $menu-width 1fr $playback-width;
grid-template-columns: $menu-width $rundown-width auto;
.info {
visibility: hidden;
@@ -74,7 +74,7 @@ $playback-width: 26rem;
grid-template-rows: 100%;
grid-template-columns: $menu-width $rundown-width;
grid-template-areas:
'sett rundown';
'sett even';
.info,
.messages,
@@ -135,7 +135,7 @@ $playback-width: 26rem;
border-radius: 8px;
height: 100%;
background-color: $bg-container-l2;
padding: 1rem;
padding: 0.8em 1.5em;
display: flex;
flex-direction: column;
}
@@ -165,7 +165,7 @@ $playback-width: 26rem;
.header {
background-color: $gray-1200;
padding: 0.5rem;
padding: 8px;
border-left: 1px solid $white-10;
border-radius: 0 8px 0 0;
}
@@ -176,14 +176,14 @@ $playback-width: 26rem;
height: 100%;
.content {
height: calc(100% - 1.5rem);
height: calc(100% - 24px);
overflow: hidden;
}
}
.info {
grid-area: info;
min-width: 17rem;
min-width: 17em;
max-width: 800px;
.content {
@@ -195,20 +195,20 @@ $playback-width: 26rem;
.messages {
grid-area: mess;
min-width: 24rem;
}
.playback {
grid-area: play;
min-height: 250px;
max-height: 380px;
min-width: 26rem;
min-width: 450px;
}
.mainContainer > .settings {
grid-area: sett;
background-color: transparent;
margin: 0;
padding: 0 0.5rem 0 0;
padding: 0 8px 0 0;
width: fit-content;
display: flex;
@@ -216,11 +216,5 @@ $playback-width: 26rem;
}
.content {
padding-top: 1.5rem;
}
.rundown {
.content {
padding-top: 0.5rem;
}
padding-top: 24px;
}
+11 -12
View File
@@ -1,22 +1,21 @@
import { lazy, useEffect } from 'react';
import { useDisclosure } from '@chakra-ui/react';
import { Box, useDisclosure } from '@chakra-ui/react';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import UploadModal from '../../common/components/upload-modal/UploadModal';
import MenuBar from '../menu/MenuBar';
import AboutModal from '../modals/about-modal/AboutModal';
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
import QuickStart from '../modals/quick-start/QuickStart';
import UploadModal from '../modals/upload-modal/UploadModal';
import SettingsModal from '../modals/settings-modal/SettingsModal';
import styles from './Editor.module.scss';
const Rundown = lazy(() => import('../rundown/RundownExport'));
const TimerControl = lazy(() => import('../control/playback/TimerControlExport'));
const MessageControl = lazy(() => import('../control/message/MessageControlExport'));
const Info = lazy(() => import('../info/InfoExport'));
const EventEditor = lazy(() => import('../event-editor/EventEditorExport'));
const IntegrationModal = lazy(() => import('../modals/integration-modal/IntegrationModal'));
const SettingsModal = lazy(() => import('../modals/settings-modal/SettingsModal'));
const Rundown = lazy(() => import('../../features/rundown/RundownExport'));
const TimerControl = lazy(() => import('../../features/control/playback/TimerControlExport'));
const MessageControl = lazy(() => import('../../features/control/message/MessageControlExport'));
const Info = lazy(() => import('../../features/info/InfoExport'));
const EventEditor = lazy(() => import('../../features/event-editor/EventEditorExport'));
export default function Editor() {
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
@@ -44,7 +43,7 @@ export default function Editor() {
<SettingsModal isOpen={isSettingsOpen} onClose={onSettingsClose} />
</ErrorBoundary>
<div className={styles.mainContainer} data-testid='event-editor'>
<div id='settings' className={styles.settings}>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar
onSettingsOpen={onSettingsOpen}
@@ -60,7 +59,7 @@ export default function Editor() {
onQuickStartOpen={onQuickStartOpen}
/>
</ErrorBoundary>
</div>
</Box>
<Rundown />
<MessageControl />
<TimerControl />
@@ -1,9 +1,9 @@
@use '../../theme/v2Styles' as *;
.eventEditor {
padding: 1rem 2rem 2rem 2rem;
padding: 16px 32px 32px 32px;
width: 100%;
gap: max(1rem, 2vh);
gap: max(16px, 2vh);
display: grid;
grid-template-areas:
@@ -15,17 +15,15 @@
.timeSettings {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 8px;
}
}
.eventInfo {
grid-area: eventInfo;
display: flex;
align-items: center;
.eventId {
margin-left:$element-spacing;
.eventId {
margin-left: $element-inner-spacing;
user-select: text;
}
}
@@ -37,7 +35,7 @@
.timeOptions {
grid-area: timeOptions;
display: flex;
gap: 1.5rem;
gap: 24px;
}
.titles {
@@ -55,19 +53,19 @@
.left {
grid-area: left;
padding: 0 1rem;
padding: 0 16px;
border-left: 1px solid $border-color-ondark;
}
.right {
padding-left: 1rem;
padding-left: 16px;
grid-area: right;
border-left: 1px solid $border-color-ondark;
}
}
@mixin input-label() {
font-size: calc(1rem - 3px);
font-size: 13px;
color: $label-gray;
}
@@ -86,32 +84,32 @@
}
&.publicToggle {
height: 2rem;
height: 32px;
display: flex;
align-items: center;
justify-items: center;
gap: 0.5rem;
gap: 8px;
}
}
.spacer {
height: 1.25rem;
height: 20px;
}
.inline {
display: flex;
align-items: center;
gap: 1rem;
gap: 16px;
}
.column {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 8px;
}
.padTop {
margin-top: 0.5rem;
margin-top: 8px;
}
.fullHeight {
@@ -42,9 +42,7 @@ export default function EventEditor() {
<div className={style.eventEditor}>
<div className={style.eventInfo}>
Event ID
<span className={style.eventId}>
<CopyTag label={event.id}>{event.id}</CopyTag>
</span>
<span className={style.eventId}>{event.id}</span>
</div>
<div className={style.eventActions}>
<CopyTag label='OSC trigger'>{`/ontime/gotoid/${event.id}`}</CopyTag>
@@ -60,7 +58,6 @@ export default function EventEditor() {
timerType={event.timerType}
/>
<EventEditorTitles
key={event.id}
eventId={event.id}
title={event.title}
presenter={event.presenter}
@@ -1,5 +1,5 @@
import { memo } from 'react';
import { IconButton } from '@chakra-ui/react';
import { Box, IconButton } from '@chakra-ui/react';
import { IoClose } from '@react-icons/all-files/io5/IoClose';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
@@ -26,7 +26,7 @@ const EventEditorExport = () => {
const removeOpenEvent = () => setEditId(null);
return (
<div className={editorStyle}>
<Box className={editorStyle}>
<ErrorBoundary>
<div className={style.eventEditorLayout}>
<EventEditor />
@@ -35,7 +35,7 @@ const EventEditorExport = () => {
</div>
</div>
</ErrorBoundary>
</div>
</Box>
);
};
@@ -24,11 +24,10 @@ export default function CountedTextArea(props: CountedTextAreaProps) {
return (
<div className={`${style.column} ${style.fullHeight}`}>
<div className={style.countedInput}>
<label className={style.inputLabel} htmlFor={field}>{label}</label>
<label className={style.inputLabel}>{label}</label>
<span className={style.charCount}>{`${value.length} characters`}</span>
</div>
<Textarea
id={field}
size='sm'
resize='none'
variant='ontime-filled'
@@ -26,11 +26,10 @@ export default function CountedTextInput(props: CountedTextInputProps) {
return (
<div className={style.column}>
<div className={style.countedInput}>
<label className={style.inputLabel} htmlFor={field}>{label}</label>
<label className={style.inputLabel}>{label}</label>
<span className={style.charCount}>{`${value.length} characters`}</span>
</div>
<Input
id={field}
size='sm'
variant='ontime-filled'
data-testid='input-textfield'
@@ -80,11 +80,8 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
return (
<div className={style.timeOptions}>
<div className={style.timers}>
<label className={inputTimeLabels} htmlFor='timeStart'>
{startLabel}
</label>
<label className={inputTimeLabels}>{startLabel}</label>
<TimeInput
id='timeStart'
name='timeStart'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
@@ -93,11 +90,8 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
placeholder='Start'
warning={warning.start}
/>
<label className={inputTimeLabels} htmlFor='timeEnd'>
{endLabel}
</label>
<label className={inputTimeLabels}>{endLabel}</label>
<TimeInput
id='timeEnd'
name='timeEnd'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
@@ -106,11 +100,8 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
placeholder='End'
warning={warning.end}
/>
<label className={style.inputLabel} htmlFor='durationOverride'>
Duration
</label>
<label className={style.inputLabel}>Duration</label>
<TimeInput
id='durationOverride'
name='durationOverride'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
@@ -6,7 +6,7 @@
font-family: $ontime-font-family;
color: $label-gray;
display: flex;
justify-content: end;
justify-content: space-between;
}
.labels {
+1
View File
@@ -33,6 +33,7 @@ export default function Info() {
return (
<>
<div className={style.panelHeader}>
<span>Ontime running on port 4001</span>
<span>{selected}</span>
</div>
<CollapsableInfo title='Network Info'>
+5 -4
View File
@@ -1,4 +1,5 @@
import { memo } from 'react';
import { Box } from '@chakra-ui/react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
@@ -10,15 +11,15 @@ import style from '../editors/Editor.module.scss';
const InfoExport = () => {
return (
<div className={style.info} data-testid='panel-info'>
<Box className={style.info} data-testid='panel-info'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'info')} />
<div className={style.content}>
<ErrorBoundary>
<Info />
</ErrorBoundary>
</div>
</div>
</Box>
);
};
}
export default memo(InfoExport);
export default memo(InfoExport)
+5 -12
View File
@@ -102,7 +102,7 @@ const MenuBar = (props: MenuBarProps) => {
return (
<VStack>
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} size='md' />
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} />
<div className={style.gap} />
<TooltipActionBtn
@@ -118,17 +118,15 @@ const MenuBar = (props: MenuBarProps) => {
icon={<IoPushOutline />}
className={isUploadOpen ? style.open : ''}
clickHandler={onUploadOpen}
tooltip='Import project file'
aria-label='Import project file'
size='sm'
tooltip='Upload showfile'
aria-label='Upload showfile'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<IoSaveOutline />}
clickHandler={downloadRundown}
tooltip='Export project file'
aria-label='Export project file'
size='sm'
tooltip='Export showfile'
aria-label='Export showfile'
/>
<div className={style.gap} />
@@ -139,7 +137,6 @@ const MenuBar = (props: MenuBarProps) => {
clickHandler={setRunMode}
tooltip='Run mode'
aria-label='Run mode'
size='sm'
/>
<TooltipActionBtn
{...buttonStyle}
@@ -148,7 +145,6 @@ const MenuBar = (props: MenuBarProps) => {
clickHandler={setEditMode}
tooltip='Edit mode'
aria-label='Edit mode'
size='sm'
/>
<div className={style.gap} />
@@ -159,7 +155,6 @@ const MenuBar = (props: MenuBarProps) => {
clickHandler={onIntegrationOpen}
tooltip='Integrations'
aria-label='Integrations'
size='sm'
/>
<TooltipActionBtn
{...buttonStyle}
@@ -168,7 +163,6 @@ const MenuBar = (props: MenuBarProps) => {
clickHandler={onSettingsOpen}
tooltip='Settings'
aria-label='Settings'
size='sm'
/>
<div className={style.gap} />
<TooltipActionBtn
@@ -178,7 +172,6 @@ const MenuBar = (props: MenuBarProps) => {
clickHandler={onAboutOpen}
tooltip='About'
aria-label='About'
size='sm'
/>
</VStack>
);
@@ -10,6 +10,7 @@ interface ModalWrapperProps {
export default function ModalWrapper(props: PropsWithChildren<ModalWrapperProps>) {
const { isOpen, onClose, title, size = 'xl', children } = props;
return (
<Modal
onClose={onClose}
@@ -12,7 +12,7 @@ interface IntegrationModalProps {
onClose: () => void;
}
const oscDocsUrl = 'https://ontime.gitbook.io/v2/control-and-feedback/integrations';
const oscDocsUrl = 'https://ontime.gitbook.io/v2/control-and-feedback/osc';
export default function IntegrationModal(props: IntegrationModalProps) {
const { isOpen, onClose } = props;
@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import type { OscSubscription } from 'ontime-types';
import type { OSCSettings, OscSubscription } from 'ontime-types';
import { TimerLifeCycle } from 'ontime-types';
import { generateId } from 'ontime-utils';
import useOscSettings, { usePostOscSubscriptions } from '../../../common/hooks-query/useOscSettings';
import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-query/useOscSettings';
import { oscPlaceholderSettings, PlaceholderSettings } from '../../../common/models/OscSettings';
import { useEmitLog } from '../../../common/stores/logger';
import ModalLoader from '../modal-loader/ModalLoader';
import OntimeModalFooter from '../OntimeModalFooter';
import OscSubscriptionRow from './OscSubscriptionRow';
@@ -33,7 +34,7 @@ const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string
},
onUpdate: {
title: 'On Every Second',
subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)',
subtitle: 'Triggers when timers are updated (at least once a second, can be more)',
},
onFinish: {
title: 'On Finish',
@@ -42,116 +43,88 @@ const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string
};
export default function OscIntegration() {
const { data, isFetching } = useOscSettings();
const { mutateAsync } = usePostOscSubscriptions();
const { data } = useOscSettings();
const { mutateAsync } = useOscSettingsMutation();
const { emitError } = useEmitLog();
const {
control,
handleSubmit,
register,
reset,
formState: { isSubmitting, isDirty, isValid },
} = useForm<OscSubscription>({
defaultValues: data.subscriptions,
values: data.subscriptions,
resetOptions: {
keepDirtyValues: true,
},
} = useForm<PlaceholderSettings>({
defaultValues: data,
values: data,
});
const [subscriptionState, setSubscription] = useState<OscSubscription>(
data?.subscriptions || oscPlaceholderSettings.subscriptions,
);
const [hasManualChange, setHasManualChange] = useState(false);
const [showSection, setShowSection] = useState<OntimeCycle>(TimerLifeCycle.onLoad);
useEffect(() => {
if (data) {
reset(data.subscriptions);
}
}, [data, reset]);
const resetForm = () => {
reset(data.subscriptions);
const originalData = data || oscPlaceholderSettings;
setSubscription(originalData.subscriptions);
// @ts-expect-error -- we know the data here is safe
reset(originalData);
};
const onSubmit = async (values: OscSubscription) => {
try {
const subscriptions = {
onLoad: values.onLoad ?? [],
onStart: values.onStart ?? [],
onPause: values.onPause ?? [],
onStop: values.onStop ?? [],
onUpdate: values.onUpdate ?? [],
onFinish: values.onFinish ?? [],
};
const deleteSubscriptionEntry = (cycle: OntimeCycle, id: string) => {
setSubscription((prev) => {
const newData = { ...prev };
newData[cycle] = [...prev[cycle].filter((el) => el.id !== id)];
return newData;
});
setHasManualChange(true);
};
await mutateAsync(subscriptions);
const addNewSubscriptionEntry = async (cycle: OntimeCycle) => {
setSubscription((prev) => {
const newData = structuredClone(prev);
newData[cycle] = [...prev[cycle], { id: generateId(), message: '', enabled: false }];
return newData;
});
setHasManualChange(true);
};
const onSubmit = async (values: OSCSettings | PlaceholderSettings) => {
try {
// @ts-expect-error -- we know of the type mismatch, not pertinent here
await mutateAsync(values);
setHasManualChange(false);
} catch (error) {
emitError(`Error setting OSC: ${error}`);
}
};
if (isFetching) {
return <ModalLoader />;
}
const subscriptionKeys = Object.keys(subscriptionState);
return (
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='osc-subscriptions'>
<OscSubscriptionRow
cycle={TimerLifeCycle.onLoad}
title={sectionText.onLoad.title}
subtitle={sectionText.onLoad.subtitle}
visible={showSection === TimerLifeCycle.onLoad}
setShowSection={setShowSection}
register={register}
control={control}
/>
<OscSubscriptionRow
cycle={TimerLifeCycle.onStart}
title={sectionText.onStart.title}
subtitle={sectionText.onStart.subtitle}
visible={showSection === TimerLifeCycle.onStart}
setShowSection={setShowSection}
register={register}
control={control}
/>
<OscSubscriptionRow
cycle={TimerLifeCycle.onPause}
title={sectionText.onPause.title}
subtitle={sectionText.onPause.subtitle}
visible={showSection === TimerLifeCycle.onPause}
setShowSection={setShowSection}
register={register}
control={control}
/>
<OscSubscriptionRow
cycle={TimerLifeCycle.onStop}
title={sectionText.onStop.title}
subtitle={sectionText.onStop.subtitle}
visible={showSection === TimerLifeCycle.onStop}
setShowSection={setShowSection}
register={register}
control={control}
/>
<OscSubscriptionRow
cycle={TimerLifeCycle.onUpdate}
title={sectionText.onUpdate.title}
subtitle={sectionText.onUpdate.subtitle}
visible={showSection === TimerLifeCycle.onUpdate}
setShowSection={setShowSection}
register={register}
control={control}
/>
<OscSubscriptionRow
cycle={TimerLifeCycle.onFinish}
title={sectionText.onFinish.title}
subtitle={sectionText.onFinish.subtitle}
visible={showSection === TimerLifeCycle.onFinish}
setShowSection={setShowSection}
register={register}
control={control}
/>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSubscriptions'>
{subscriptionKeys.map((cycle, idx) => {
return (
<div key={`${cycle}-${idx}`}>
<OscSubscriptionRow
key={cycle}
cycle={cycle as TimerLifeCycle}
title={sectionText[cycle as TimerLifeCycle].title}
subtitle={sectionText[cycle as TimerLifeCycle].subtitle}
visible={showSection === cycle}
setShowSection={setShowSection}
subscriptionOptions={subscriptionState[cycle as TimerLifeCycle]}
handleDelete={deleteSubscriptionEntry}
handleAddNew={addNewSubscriptionEntry}
register={register}
/>
{idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />}
</div>
);
})}
<OntimeModalFooter
formId='osc-subscriptions'
formId='oscSubscriptions'
handleRevert={resetForm}
isDirty={isDirty}
isDirty={isDirty || hasManualChange}
isValid={isValid}
isSubmitting={isSubmitting}
/>
@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { FormControl, Input, Switch } from '@chakra-ui/react';
@@ -6,13 +5,12 @@ import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-qu
import { PlaceholderSettings } from '../../../common/models/OscSettings';
import { useEmitLog } from '../../../common/stores/logger';
import { isIPAddress, isOnlyNumbers } from '../../../common/utils/regex';
import ModalLoader from '../modal-loader/ModalLoader';
import OntimeModalFooter from '../OntimeModalFooter';
import styles from '../Modal.module.scss';
export default function OscSettings() {
const { data, isFetching } = useOscSettings();
const { data } = useOscSettings();
const { mutateAsync } = useOscSettingsMutation();
const { emitError } = useEmitLog();
const {
@@ -24,16 +22,8 @@ export default function OscSettings() {
} = useForm<PlaceholderSettings>({
defaultValues: data,
values: data,
resetOptions: {
keepDirtyValues: true,
},
});
useEffect(() => {
if (data) {
reset(data);
}
}, [data, reset]);
const onSubmit = async (values: PlaceholderSettings) => {
const numericPortIn = Number(values.portIn);
const numericPortOut = Number(values.portOut);
@@ -61,10 +51,6 @@ export default function OscSettings() {
reset(data);
};
if (isFetching) {
return <ModalLoader />;
}
return (
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSettings'>
<div className={styles.splitSection}>
@@ -1,10 +1,8 @@
import { Control, useFieldArray, UseFormRegister } from 'react-hook-form';
import { UseFormRegister } from 'react-hook-form';
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { OscSubscription, TimerLifeCycle } from 'ontime-types';
import { useEmitLog } from '../../../common/stores/logger';
import { OscSubscriptionOptions, TimerLifeCycle } from 'ontime-types';
import collapseStyles from '../../../common/components/collapse-bar/CollapseBar.module.scss';
import styles from '../Modal.module.scss';
@@ -15,50 +13,37 @@ interface OscSubscriptionRowProps {
subtitle: string;
visible: boolean;
setShowSection: (cycle: TimerLifeCycle) => void;
register: UseFormRegister<OscSubscription>;
control: Control<OscSubscription>;
subscriptionOptions: OscSubscriptionOptions[];
handleDelete: (cycle: TimerLifeCycle, id: string) => void;
handleAddNew: (cycle: TimerLifeCycle) => void;
register: UseFormRegister<any>;
}
export default function OscSubscriptionRow(props: OscSubscriptionRowProps) {
const { cycle, title, subtitle, visible, setShowSection, register, control } = props;
const { emitError } = useEmitLog();
const { fields, append, remove } = useFieldArray({
name: cycle,
control,
});
const { cycle, title, subtitle, visible, setShowSection, subscriptionOptions, handleDelete, handleAddNew, register } =
props;
const hasTooManyOptions = fields.length >= 3;
const hasTooManyOptions = subscriptionOptions.length >= 3;
const headerStyle = `${styles.splitSection} ${visible ? '' : styles.showPointer}`;
const sectionTitle = `${title} ${fields.length ? fields.length : '-'} / 3`;
const handleAddNew = () => {
if (hasTooManyOptions) {
emitError('Maximum amount of onLoad subscriptions reached (3)');
return;
}
append({
message: '',
enabled: false,
});
};
const registerPrefix = `subscriptions.${cycle}`;
return (
<>
<div className={headerStyle} onClick={() => setShowSection(cycle)}>
<div>
<span className={`${styles.sectionTitle} ${styles.main}`}>{sectionTitle}</span>
<span className={`${styles.sectionTitle} ${styles.main}`}>{title}</span>
{visible && <span className={styles.sectionSubtitle}>{subtitle}</span>}
</div>
<FiChevronUp className={visible ? collapseStyles.moreCollapsed : collapseStyles.moreExpanded} />
</div>
{visible && (
<>
{fields.map((subscription, index) => (
<div key={subscription.id} className={styles.entryRow}>
{subscriptionOptions.map((option, idx) => (
<div key={option.id} className={styles.entryRow}>
<input type='hidden' {...register(`${registerPrefix}[${idx}].id`)} value={option.id} />
<IconButton
icon={<IoRemove />}
onClick={() => remove(index)}
onClick={() => handleDelete(cycle, option.id)}
aria-label='delete'
size='xs'
colorScheme='red'
@@ -67,14 +52,13 @@ export default function OscSubscriptionRow(props: OscSubscriptionRowProps) {
placeholder='OSC Message'
size='xs'
variant='ontime-filled-on-light'
autoComplete='off'
{...register(`${cycle}.${index}.message`)}
{...register(`${registerPrefix}[${idx}].message`)}
/>
<Switch variant='ontime-on-light' {...register(`${cycle}.${index}.enabled`)} />
<Switch variant='ontime-on-light' {...register(`${registerPrefix}[${idx}].enabled`)} />
</div>
))}
<Button
onClick={handleAddNew}
onClick={() => handleAddNew(cycle)}
className={styles.shiftRight}
isDisabled={hasTooManyOptions}
size='xs'
@@ -1,46 +0,0 @@
@use '../../../theme/ontimeColours' as *;
.screenLoader {
position: absolute;
z-index: 10;
width: 100%;
height: 100%;
display: grid;
place-content: center;
top: 0;
background-color: $white-60;
}
$loader-size: 48px;
.loader {
width: $loader-size;
height: $loader-size;
background: $blue-500;
display: inline-block;
border-radius: 50%;
box-sizing: border-box;
animation: animloader 1s ease-in infinite;
}
@keyframes animloader {
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes animloader {
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@@ -1,9 +0,0 @@
import style from './ModalLoader.module.scss';
export default function ModalLoader() {
return (
<div className={style.screenLoader}>
<span className={style.loader} />
</div>
);
}
@@ -2,3 +2,10 @@ export const inputProps = {
size: 'sm',
autoComplete: 'off',
};
export const portInputProps = {
...inputProps,
type: 'number',
min: '1024',
max: '65535',
};
@@ -1,10 +1,6 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Button,
Input,
Modal,
@@ -38,27 +34,16 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
register,
reset,
formState: { isSubmitting },
} = useForm({
defaultValues: data,
resetOptions: {
keepDirtyValues: true,
},
});
} = useForm({ defaultValues: data });
useEffect(() => {
if (data) reset(data);
}, [data, reset]);
reset(data);
}, [reset, data]);
const onSubmit = async (data: Partial<EventData>) => {
try {
await postNew(data);
await ontimeQueryClient.invalidateQueries(EVENT_DATA);
await ontimeQueryClient.invalidateQueries(RUNDOWN_TABLE);
onClose();
} catch (_) {
/* WE DO NOT HANDLE ERRORS */
}
await postNew(data);
await ontimeQueryClient.invalidateQueries(EVENT_DATA);
await ontimeQueryClient.invalidateQueries(RUNDOWN_TABLE);
};
const onReset = () => reset(eventDataPlaceholder);
@@ -81,15 +66,6 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
<ModalCloseButton />
<ModalBody className={styles.pad}>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer}>
<Alert status='info' variant='ontime-on-light-info'>
<AlertIcon />
<div className={styles.column}>
<AlertTitle>Note</AlertTitle>
<AlertDescription>
On submit, application options will be kept but rundown and event data will be reset
</AlertDescription>
</div>
</Alert>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Event title
@@ -148,6 +124,9 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
/>
</label>
</div>
<div className={styles.footerNotes}>
Note: Application options will be kept but rundown and event data will be reset <br />
</div>
<ModalFooter className={styles.buttonSection}>
<Button onClick={onReset} isDisabled={disableButtons} variant='ontime-ghost-on-light' size='sm'>
Clear data
@@ -160,7 +139,7 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
padding='0 2em'
size='sm'
>
New project file
New showfile
</Button>
</ModalFooter>
</form>
@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { Alert, AlertDescription, AlertIcon, AlertTitle, Button, IconButton, Input, Switch } from '@chakra-ui/react';
import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
@@ -10,7 +9,6 @@ import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBt
import useAliases from '../../../common/hooks-query/useAliases';
import { useEmitLog } from '../../../common/stores/logger';
import { handleLinks } from '../../../common/utils/linkUtils';
import ModalLoader from '../modal-loader/ModalLoader';
import { inputProps } from '../modalHelper';
import ModalLink from '../ModalLink';
import OntimeModalFooter from '../OntimeModalFooter';
@@ -23,7 +21,7 @@ const aliasesDocsUrl = 'https://ontime.gitbook.io/v2/features/url-aliases';
type Aliases = { aliases: Alias[] };
export default function AliasesForm() {
const { data, status, isFetching, refetch } = useAliases();
const { data, status, refetch } = useAliases();
const { emitError } = useEmitLog();
const {
control,
@@ -34,21 +32,12 @@ export default function AliasesForm() {
} = useForm<Aliases>({
defaultValues: { aliases: data },
values: { aliases: data || [] },
resetOptions: {
keepDirtyValues: true,
},
});
const { fields, append, remove } = useFieldArray({
name: 'aliases',
control,
});
useEffect(() => {
if (data) {
reset(data);
}
}, [data, reset]);
const onSubmit = async (formData: Aliases) => {
try {
await postAliases(formData.aliases);
@@ -65,7 +54,7 @@ export default function AliasesForm() {
const addNew = () => {
if (fields.length > 20) {
emitError('Maximum amount of aliases reached (20)');
emitError('Maximum amount of aliases reacted (20)');
return;
}
append({
@@ -78,10 +67,6 @@ export default function AliasesForm() {
const disableInputs = status === 'loading';
const hasTooManyOptions = fields.length >= 20;
if (isFetching) {
return <ModalLoader />;
}
return (
<form onSubmit={handleSubmit(onSubmit)} id='aliases' className={style.sectionContainer}>
<div style={{ height: '16px' }} />
@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Input, Select } from '@chakra-ui/react';
import type { Settings } from 'ontime-types';
@@ -7,7 +6,6 @@ import { postSettings } from '../../../common/api/ontimeApi';
import useSettings from '../../../common/hooks-query/useSettings';
import { useEmitLog } from '../../../common/stores/logger';
import { isOnlyNumbers } from '../../../common/utils/regex';
import ModalLoader from '../modal-loader/ModalLoader';
import ModalSplitInput from '../ModalSplitInput';
import OntimeModalFooter from '../OntimeModalFooter';
@@ -16,7 +14,7 @@ import ModalPinInput from './ModalPinInput';
import style from './SettingsModal.module.scss';
export default function AppSettingsModal() {
const { data, status, isFetching, refetch } = useSettings();
const { data, status, refetch } = useSettings();
const { emitError } = useEmitLog();
const {
handleSubmit,
@@ -26,17 +24,8 @@ export default function AppSettingsModal() {
} = useForm<Settings>({
defaultValues: data,
values: data,
resetOptions: {
keepDirtyValues: true,
},
});
useEffect(() => {
if (data) {
reset(data);
}
}, [data, reset]);
const onSubmit = async (formData: Settings) => {
try {
await postSettings(formData);
@@ -53,10 +42,6 @@ export default function AppSettingsModal() {
const disableInputs = status === 'loading';
if (isFetching) {
return <ModalLoader />;
}
return (
<form onSubmit={handleSubmit(onSubmit)} id='app-settings' className={style.sectionContainer}>
<ModalSplitInput
@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Alert, AlertDescription, AlertIcon, AlertTitle, Input } from '@chakra-ui/react';
import { UserFields } from 'ontime-types';
@@ -6,7 +5,6 @@ import { UserFields } from 'ontime-types';
import { postUserFields } from '../../../common/api/ontimeApi';
import useUserFields from '../../../common/hooks-query/useUserFields';
import { useEmitLog } from '../../../common/stores/logger';
import ModalLoader from '../modal-loader/ModalLoader';
import { inputProps } from '../modalHelper';
import ModalLink from '../ModalLink';
import ModalSplitInput from '../ModalSplitInput';
@@ -17,7 +15,7 @@ import style from './SettingsModal.module.scss';
const userFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields';
export default function CuesheetSettings() {
const { data, status, isFetching, refetch } = useUserFields();
const { data, status, refetch } = useUserFields();
const { emitError } = useEmitLog();
const {
handleSubmit,
@@ -27,17 +25,8 @@ export default function CuesheetSettings() {
} = useForm<UserFields>({
defaultValues: data,
values: data,
resetOptions: {
keepDirtyValues: true,
},
});
useEffect(() => {
if (data) {
reset(data);
}
}, [data, reset]);
const onSubmit = async (formData: UserFields) => {
try {
await postUserFields(formData);
@@ -54,10 +43,6 @@ export default function CuesheetSettings() {
const disableInputs = status === 'loading';
if (isFetching) {
return <ModalLoader />;
}
return (
<form onSubmit={handleSubmit(onSubmit)} id='cuesheet-settings' className={style.sectionContainer}>
<div style={{ height: '16px' }} />
@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Input, Textarea } from '@chakra-ui/react';
import { EventData } from 'ontime-types';
@@ -6,7 +5,6 @@ import { EventData } from 'ontime-types';
import { postEventData } from '../../../common/api/eventDataApi';
import useEventData from '../../../common/hooks-query/useEventData';
import { useEmitLog } from '../../../common/stores/logger';
import ModalLoader from '../modal-loader/ModalLoader';
import { inputProps } from '../modalHelper';
import ModalInput from '../ModalInput';
import OntimeModalFooter from '../OntimeModalFooter';
@@ -14,7 +12,7 @@ import OntimeModalFooter from '../OntimeModalFooter';
import style from './SettingsModal.module.scss';
export default function EventDataForm() {
const { data, status, isFetching, refetch } = useEventData();
const { data, status, refetch } = useEventData();
const { emitError } = useEmitLog();
const {
handleSubmit,
@@ -24,17 +22,8 @@ export default function EventDataForm() {
} = useForm<EventData>({
defaultValues: data,
values: data,
resetOptions: {
keepDirtyValues: true,
},
});
useEffect(() => {
if (data) {
reset(data);
}
}, [data, reset]);
const onSubmit = async (formData: EventData) => {
try {
await postEventData(formData);
@@ -51,10 +40,6 @@ export default function EventDataForm() {
const disableInputs = status === 'loading';
if (isFetching) {
return <ModalLoader />;
}
return (
<form onSubmit={handleSubmit(onSubmit)} id='event-data' className={style.sectionContainer}>
<ModalInput

Some files were not shown because too many files have changed in this diff Show More