Compare commits

..

57 Commits

Author SHA1 Message Date
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
615 changed files with 14663 additions and 27137 deletions
-1
View File
@@ -1 +0,0 @@
"ONTIME_VERSION.js"
+4 -47
View File
@@ -1,5 +1,4 @@
{
"root": true,
"parserOptions": {
"ecmaVersion": 2020
},
@@ -7,56 +6,14 @@
"es6": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"eslint-config-prettier"
],
"plugins": [
"@typescript-eslint",
"prettier"
"eslint:recommended"
],
"overrides": [
{
"files": [
"e2e/**/**.spec.ts",
"e2e/**/**.test.ts"
],
"extends": [
"plugin:playwright/playwright-test"
]
"files": ["e2e/**/**.spec.ts", "e2e/**/**.test.ts"],
"extends": ["plugin:playwright/playwright-test"]
}
],
"rules": {
"no-useless-concat": "warn",
"prefer-template": "warn",
"no-console": [
"warn",
{
"allow": [
"warn",
"error"
]
}
],
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"prettier/prettier": [
"warn",
{
"endOfLine": "auto"
}
]
}
"rules": {}
}
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 }}
+7 -20
View File
@@ -2,7 +2,7 @@ name: Docker Image CI Ontime V2
on:
push:
tags: [ "*" ]
tags: [ "v2.*.*" ]
workflow_dispatch:
jobs:
@@ -16,16 +16,15 @@ jobs:
- uses: actions/checkout@v3
- name: Setup env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Setup Node.js environment
uses: actions/setup-node@v3.6.0
with:
version: 16.16.0
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -34,7 +33,7 @@ jobs:
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm turbo build:docker
- name: Docker Login
uses: docker/login-action@v2.1.0
with:
@@ -44,8 +43,7 @@ jobs:
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Build and push stable release
if: github.event.release.prerelease == false
- name: Build and push Docker images
uses: docker/build-push-action@v4.0.0
with:
context: .
@@ -53,16 +51,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
- name: Build and push pre-release
if: github.event.release.prerelease == true
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./Dockerfile
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:nightly
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:beta_${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:beta_v2
+8 -10
View File
@@ -2,7 +2,7 @@ name: Ontime build v2
on:
push:
tags: [ "*" ]
tags: [ "v2.*.*" ]
workflow_dispatch:
jobs:
@@ -17,9 +17,9 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -35,9 +35,7 @@ jobs:
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
./apps/electron/dist/ontime-macOS-x64.dmg
./apps/electron/dist/ontime-macOS-arm64.dmg
files: './apps/electron/dist/ontime-macOS.dmg'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -52,9 +50,9 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -85,9 +83,9 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
+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
+5 -29
View File
@@ -2,7 +2,7 @@ name: Ontime test v2
on:
pull_request:
branches: '*'
branches: [ v2 ]
workflow_dispatch:
jobs:
@@ -20,47 +20,23 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Run code quality per package
- name: React - Run linter + TypeScript checks
if: always()
run: pnpm lint && tsc --noEmit
working-directory: ./apps/client
- name: Server - Run linter + TypeScript checks
if: always()
run: pnpm lint && tsc --noEmit
working-directory: ./apps/server
- name: Utils - Run linter + TypeScript checks
if: always()
run: pnpm lint && tsc --noEmit
working-directory: ./packages/utils
- name: Types - Run linter
if: always()
run: pnpm lint
working-directory: ./packages/types
# We choose to run tests separately
- name: React - Run unit tests
if: always()
run: pnpm test:pipeline
working-directory: ./apps/client
- name: Server - Run unit tests
if: always()
run: pnpm test:pipeline
working-directory: ./apps/server
- name: Utils - Run unit tests
if: always()
run: pnpm test:pipeline
working-directory: ./packages/utils
@@ -76,9 +52,9 @@ jobs:
node-version: 16
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v2.2.4
with:
version: 8
version: 7.26.3
- name: Install dependencies
run: pnpm install --frozen-lockfile
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm lint-staged
BIN
View File
Binary file not shown.
-10
View File
@@ -1,10 +0,0 @@
build
coverage
dist
node_modules
playwright-report
**/*.toml
**/*.yml
**/*.json
-1
View File
@@ -1,5 +1,4 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
+4 -56
View File
@@ -1,82 +1,33 @@
# GETTING STARTED
Ontime consists of 3 distinct parts
- __client__: A React app for Ontime's UI and web clients
- __client__: A React app for Ontime's UI and web clients
- __electron__: An electron app which facilitates the cross-platform distribution of Ontime
- __server__: A node application which handles the domains services and integrations
The steps below will assume you have locally installed the necessary dependencies.
The steps below will assume you have locally installed the necessary dependencies.
Other dependencies will be installed as part of the setup
- __node__ (>=16.16)
- __pnpm__ (>=7)
- __docker__ (only necessary to run and build docker images)
## LOCAL DEVELOPMENT
The electron app is only necessary to distribute an installable version of the app and is not required for local
development.
The electron app is only necessary to distribute an installable version of the app and is not required for local development.
Locally, we would need to run both the React client and the node.js server in development mode
From the project root, run the following commands
- __Install the project dependencies__ by running `pnpm i`
- __Run dev mode__ by running `turbo dev`
### Debugging backend
To debug backend code in Node.js:
- Open two separate terminals and navigate to the `apps/client` and `apps/server` directories.
- In each terminal, run the command `pnpm dev` to start the development servers for both the client and server
applications.
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by
running `pnpm dev:inspect`.
## TESTING
Generally we have 2 types of tests.
- Unit tests for functions that contain business logic
- End-to-end tests for core features
### Unit tests
Unit tests are contained in mostly all the apps and packages (client, server and utils)
You can run unit tests by running turbo `turbo test:pipeline` from the project root.
This will run all tests and close test runner.
Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode
### E2E tests
E2E tests are in a separate package. On running, [playwright](https://playwright.dev/) will spin up an instance of the
webserver to test against
These tests also run against a separate version of the DB (test-db)
Before running the E2E, you should first build the project with `pnpm build:local`.
You can run playwright tests from project root with `pnpm e2e`
When writing tests, it can be handy to run playwright in interactive mode with `pnpm e2e:i`. You would need to manually
start the webserver with `pnpm dev:server`
Some other useful commands
- `pnpm e2e --ui` open playwright UI
- `pnpm e2e --headed` run tests with a visible browser window
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
Ontime uses Electron to distribute the application.
You can generate a distribution for your OS by running the following steps.
From the project root, run the following commands
- __Install the project dependencies__ by running `pnpm i`
- __Build the UI and server__ by running `turbo build:electron`
- __Build the UI and server__ by running `turbo build`
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
The build distribution assets will be at `.apps/electron/dist`
@@ -87,13 +38,10 @@ Ontime provides a docker-compose file to aid with building and running docker im
While it should allow for a generic setup, it might need to be modified to fit your infrastructure.
From the project root, run the following commands
- __Install the project dependencies__ by running `pnpm i`
- __Build packages__ by running `pnpm build:localdocker`
- __Build docker image from__ by running `docker build -t getontime/ontime`
- __Run docker image from compose__ by running `docker-compose up -d`
Other useful commands
- __List running processes__ by running `docker ps`
- __Kill running process__ by running `docker kill <process-id>`
+69 -59
View File
@@ -1,40 +1,37 @@
[![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)
[![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)
[![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://cpvalente.gitbook.io/ontime/)
## 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-arm64.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 viewers 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
Once installed and running, Ontime starts a background server that is the heart of all processes.
From the app, you can add / edit your running order and control the timer playback.
Any device with a browser in the same network can choose one of the supported views to render the available data.
Any device with a browser in the same network can choose one of the supported views to render the available data.
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
@@ -51,53 +48,50 @@ IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock
```
```
For management views
For management views
-------------------------------------------------------------
IP.ADDRESS:4001/editor > the control interface, same as the app
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
```
More documentation is available [in our docs](https://ontime.gitbook.io)
More documentation is available [in our docs](https://cpvalente.gitbook.io/ontime/)
## Feature List (in no specific order)
- [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)
- Stage Timer
- Backstage Info
- Public Info
- Studio Clock
- [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
- [x] Multi-platform (available on Windows, MacOS and Linux)
- [x] [Countdown to anything!](https://cpvalente.gitbook.io/ontime/views/countdown): have
a countdown to any scheduled event
- [x] Multi-platform (available on Windows, MacOS and Linux)
- [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime)
## 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
@@ -115,22 +109,40 @@ Taking advantage of the integrations, we currently use Ontime with:
### Make your own viewer
Ontime broadcasts its data over WebSockets. This allows you to consume its data outside the application.
Ontime broadcasts its data over WebSockets. This allows you to consume its data outside of the application.
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language
that can run in the browser).
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language that can run in the browser).
<br />
See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on
See [this repository](https://github.com/cpvalente/ontime-viewer-template) 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://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api)
### Headless run
You can self-host and run Ontime in a docker image.
You can self-host and run Ontime in a docker image. The run command will:
The docker image along with documentation is [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
- expose the necessary ports (listed in the Dockerfile)
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
- the image name __getontime/ontime__
If you want to run this image in a Raspberry Pi, please see [the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi)
The docker image is
in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
```bash
docker pull getontime/ontime
```
```bash
# Port 4001 - ontime server port
# Port 8888 - OSC input, bound to localhost IP Address
docker run -p 4001:4001 -p 127.0.0.1:8888:8888/udp --mount type=bind,source="$(pwd)/ontime-db",target=/server/preloaded-db getontime/ontime
```
or if running from the docker compose
```bash
docker-compose up
```
## Roadmap
@@ -138,32 +150,32 @@ If you want to run this image in a Raspberry Pi, please see [the docs](https://o
Several features are planned in the roadmap, and we continuously adjust this to match how users interact with the app.
<br />
Have an idea? Reach out via [email](mail@getontime.no)
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
Have an idea? Reach out via [email](mail@getontime.no) or [open an issue](https://github.com/cpvalente/ontime/issues/new)
### Issues
We use Github's issue tracking for bug reporting and feature requests. <br />
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
#### Unsigned App
When installing the app you would see warning screens from the Operating System like:
`Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.`
```Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.```
or
`Ontime can't be opened because it is from an unidentified developer`
```Ontime can't be opened because it is from an unidentified developer```
or in Linux
`Could Not Display "ontime-linux.AppImage`
```Could Not Display "ontime-linux.AppImage```
You can circumvent this by allowing the execution of the app manually.
- In Windows: click more and select "Run Anyway"
- in macOS: the solution in macOS is different across versions, please refer to the [apple documentation](https://support.apple.com/en-gb/guide/mac-help/mh40616/mac)
- in macOS: after attempting to run the installer, navigate to System Preferences -> Security &
Privacy and allow the execution of the app
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
File as a Program
@@ -175,7 +187,6 @@ please [open an issue](https://github.com/cpvalente/ontime/issues/new)
#### Safari
There are known issues with Safari versions lower than 13:
- Spacing and text styles might have small inconsistencies
- Table view does not work
@@ -185,14 +196,13 @@ There is no plan for any further work on this.
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
<br /><br />
If you are a developer and would like to contribute with some code, please open an issue to discuss before opening a
Pull Request.
If you are a developer and would like to contribute with some code, please open an issue to discuss before opening a Pull Request.
<br />
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
# Help
Help is underway! ... and can be found [here](https://ontime.gitbook.io)
Help is underway! ... and can be found [here](https://cpvalente.gitbook.io/ontime/)
# License
+17 -2
View File
@@ -8,18 +8,33 @@
"browser": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended"
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier",
"plugin:@tanstack/eslint-plugin-query/recommended",
"prettier"
],
"plugins": [
"react",
"testing-library",
"simple-import-sort",
"@tanstack/query"
"@tanstack/query",
"@typescript-eslint",
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
],
"no-useless-concat": "warn",
"prefer-template": "warn",
"react/jsx-no-bind": [
"error",
{
-1
View File
@@ -1,5 +1,4 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
+6
View File
@@ -0,0 +1,6 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-prettier"
]
}
+33 -30
View File
@@ -1,33 +1,33 @@
{
"name": "ontime-ui",
"version": "2.28.12",
"version": "2.0.0-beta1",
"private": true,
"dependencies": {
"@chakra-ui/react": "^2.7.0",
"@chakra-ui/react": "^2.5.1",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@react-icons/all-files": "^4.1.0",
"@sentry/react": "^7.46.0",
"@sentry/tracing": "^7.46.0",
"@tanstack/react-query": "^5.8.4",
"@tanstack/react-query-devtools": "^5.8.4",
"@tanstack/react-table": "^8.9.2",
"autosize": "^6.0.1",
"@sentry/react": "^7.28.1",
"@sentry/tracing": "^7.24.1",
"@tanstack/react-query": "^4.26.1",
"@tanstack/react-query-devtools": "^4.26.1",
"autosize": "^5.0.2",
"axios": "^1.2.0",
"color": "^4.2.3",
"csv-stringify": "^6.2.3",
"deepmerge": "^4.3.0",
"framer-motion": "^10.10.0",
"framer-motion": "^8.0.2",
"luxon": "^3.3.0",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-fast-compare": "^3.2.0",
"react-hook-form": "^7.43.5",
"react-qr-code": "^2.0.11",
"react-router-dom": "^6.3.0",
"react-table": "^7.7.0",
"typeface-open-sans": "^1.1.13",
"web-vitals": "^3.1.1",
"zustand": "^4.3.6"
@@ -38,11 +38,9 @@
"dev": "cross-env BROWSER=none vite",
"build": "vite build",
"build:local": "cross-env NODE_ENV=local vite build",
"build:electron": "cross-env NODE_ENV=local vite build",
"build:docker": "vite build",
"build:localdocker": "cross-env NODE_ENV=local vite build",
"lint": "eslint . --quiet",
"lint-staged": "eslint",
"lint": "eslint .",
"stylelint": "npx stylelint \"**/*.scss\"\n",
"test": "vitest",
"test:pipeline": "vitest run",
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build"
@@ -60,22 +58,24 @@
]
},
"devDependencies": {
"@sentry/vite-plugin": "^0.4.0",
"@tanstack/eslint-plugin-query": "^5.8.4",
"@sentry/vite-plugin": "^0.3.0",
"@tanstack/eslint-plugin-query": "^4.26.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^14.1.1",
"@types/color": "^3.0.3",
"@types/luxon": "^3.2.0",
"@types/prop-types": "^15.7.5",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@types/testing-library__jest-dom": "^5.14.5",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"@vitejs/plugin-react": "^3.0.1",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-jest": "^27.1.7",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
@@ -83,13 +83,16 @@
"jsdom": "^21.1.0",
"ontime-types": "workspace:*",
"ontime-utils": "workspace:*",
"prettier": "^3.0.3",
"prettier": "^2.8.3",
"prop-types": "^15.8.1",
"sass": "^1.57.1",
"typescript": "^5.2.2",
"vite": "^4.3.1",
"vite-plugin-compression2": "^0.9.0",
"stylelint": "^14.16.1",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-standard-scss": "^6.1.0",
"typescript": "^4.9.4",
"vite": "^4.0.4",
"vite-plugin-svgr": "^2.4.0",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.30.1"
"vite-tsconfig-paths": "^4.0.3",
"vitest": "^0.29.8"
}
}
+17 -23
View File
@@ -1,43 +1,39 @@
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';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ContextMenu } from './common/components/context-menu/ContextMenu';
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
import { AppContextProvider } from './common/context/AppContext';
import useElectronEvent from './common/hooks/useElectronEvent';
import { ontimeQueryClient } from './common/queryClient';
import { socketClientName } from './common/stores/connectionName';
import { connectSocket } from './common/utils/socket';
import theme from './theme/theme';
import { TranslationProvider } from './translation/TranslationProvider';
import AppRouter from './AppRouter';
// Load Open Sans typeface
// @ts-expect-error no types from font import
import('typeface-open-sans');
const preferredClientName = socketClientName.getState().name;
connectSocket(preferredClientName);
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);
}
@@ -46,7 +42,7 @@ function App() {
document.removeEventListener('keydown', handleKeyPress);
}
};
}, [isElectron, sendToElectron]);
}, []);
return (
<ChakraProvider resetCSS theme={theme}>
@@ -55,11 +51,9 @@ function App() {
<BrowserRouter>
<div className='App'>
<ErrorBoundary>
<TranslationProvider>
<ContextMenu>
<AppRouter />
</ContextMenu>
</TranslationProvider>
<Suspense fallback={null}>
<AppRouter />
</Suspense>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
+87 -77
View File
@@ -1,12 +1,11 @@
import { lazy, Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { lazy, useEffect } from 'react';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import withAlias from './features/AliasWrapper';
import useAliases from './common/hooks-query/useAliases';
import withData from './features/viewers/ViewWrapper';
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
const Operator = lazy(() => import('./features/operator/Operator'));
const Table = lazy(() => import('./features/table/ProtectedTable'));
const TimerView = lazy(() => import('./features/viewers/timer/Timer'));
const MinimalTimerView = lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
@@ -18,92 +17,103 @@ const Public = lazy(() => import('./features/viewers/public/Public'));
const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper'));
const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock'));
const STimer = withAlias(withData(TimerView));
const SMinimalTimer = withAlias(withData(MinimalTimerView));
const SClock = withAlias(withData(ClockView));
const SCountdown = withAlias(withData(Countdown));
const SBackstage = withAlias(withData(Backstage));
const SPublic = withAlias(withData(Public));
const SLowerThird = withAlias(withData(Lower));
const SStudio = withAlias(withData(StudioClock));
const STimer = withData(TimerView);
const SMinimalTimer = withData(MinimalTimerView);
const SClock = withData(ClockView);
const SCountdown = withData(Countdown);
const SBackstage = withData(Backstage);
const SPublic = withData(Public);
const SLowerThird = withData(Lower);
const SStudio = withData(StudioClock);
const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper'));
const FeatureWrapper = lazy(() => import('./features/FeatureWrapper'));
const RundownPanel = 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'));
export default function AppRouter() {
const { data } = useAliases();
const location = useLocation();
const navigate = useNavigate();
// navigate if is alias route
useEffect(() => {
if (!data) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
break;
}
}
}, [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 />} />
<Route path='/op' element={<Operator />} />
<Route path='/operator' element={<Operator />} />
{/*/!* 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={<Cuesheet />} />
<Route path='/cuelist' element={<Cuesheet />} />
<Route path='/table' element={<Cuesheet />} />
{/*/!* 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={
<FeatureWrapper>
<RundownPanel />
</FeatureWrapper>
}
/>
<Route
path='/timercontrol'
element={
<FeatureWrapper>
<TimerControl />
</FeatureWrapper>
}
/>
<Route
path='/messagecontrol'
element={
<FeatureWrapper>
<MessageControl />
</FeatureWrapper>
}
/>
<Route
path='/info'
element={
<FeatureWrapper>
<Info />
</FeatureWrapper>
}
/>
{/*/!* 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,
},
},
});
+24
View File
@@ -0,0 +1,24 @@
// Exported viewer link location
const minimalLocation = 'minimal';
const speakerLocation = 'speaker';
const smLocation = 'sm';
const publicLocation = 'public';
const pipLocation = 'pip';
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: pipLocation, label: 'Picture in Picture' },
{ link: studioLocation, label: 'Studio clock' },
{ link: countdownLocation, label: 'Countdown' },
{ link: cuesheetLocation, label: 'Cuesheet' },
];
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,21 +0,0 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="256" height="256" rx="24" fill="#222222"/>
<path d="M40.5 131.781V128.193C40.5 114.635 42.4369 102.159 46.3106 90.7659C50.1843 79.2587 55.8239 69.2896 63.2295 60.8586C70.6351 52.4277 79.7497 45.8765 90.5733 41.2053C101.397 36.4202 113.815 34.0276 127.829 34.0276C141.843 34.0276 154.318 36.4202 165.256 41.2053C176.193 45.8765 185.365 52.4277 192.771 60.8586C200.29 69.2896 205.987 79.2587 209.86 90.7659C213.734 102.159 215.671 114.635 215.671 128.193V131.781C215.671 145.226 213.734 157.701 209.86 169.208C205.987 180.601 200.29 190.571 192.771 199.115C185.365 207.546 176.25 214.098 165.427 218.769C154.603 223.44 142.185 225.776 128.171 225.776C114.157 225.776 101.682 223.44 90.7442 218.769C79.9206 214.098 70.7491 207.546 63.2295 199.115C55.8239 190.571 50.1843 180.601 46.3106 169.208C42.4369 157.701 40.5 145.226 40.5 131.781ZM89.7188 128.193V131.781C89.7188 139.529 90.4024 146.764 91.7696 153.486C93.1368 160.208 95.3015 166.132 98.2637 171.259C101.34 176.272 105.328 180.203 110.227 183.051C115.126 185.899 121.107 187.323 128.171 187.323C135.007 187.323 140.874 185.899 145.773 183.051C150.673 180.203 154.603 176.272 157.565 171.259C160.528 166.132 162.692 160.208 164.06 153.486C165.541 146.764 166.281 139.529 166.281 131.781V128.193C166.281 120.673 165.541 113.609 164.06 107.001C162.692 100.279 160.471 94.3547 157.395 89.2278C154.432 83.9869 150.502 79.8853 145.603 76.9231C140.703 73.9609 134.779 72.4797 127.829 72.4797C120.879 72.4797 114.955 73.9609 110.056 76.9231C105.271 79.8853 101.34 83.9869 98.2637 89.2278C95.3015 94.3547 93.1368 100.279 91.7696 107.001C90.4024 113.609 89.7188 120.673 89.7188 128.193Z" fill="#FFFFFA"/>
<mask id="mask0_30_31" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="40" y="34" width="176" height="192">
<path d="M40.5 131.781V128.193C40.5 114.635 42.4369 102.159 46.3106 90.7659C50.1843 79.2587 55.8239 69.2896 63.2295 60.8586C70.6351 52.4277 79.7497 45.8765 90.5733 41.2053C101.397 36.4202 113.815 34.0276 127.829 34.0276C141.843 34.0276 154.318 36.4202 165.256 41.2053C176.193 45.8765 185.365 52.4277 192.771 60.8586C200.29 69.2896 205.987 79.2587 209.86 90.7659C213.734 102.159 215.671 114.635 215.671 128.193V131.781C215.671 145.226 213.734 157.701 209.86 169.208C205.987 180.601 200.29 190.571 192.771 199.115C185.365 207.546 176.25 214.098 165.427 218.769C154.603 223.44 142.185 225.776 128.171 225.776C114.157 225.776 101.682 223.44 90.7442 218.769C79.9206 214.098 70.7491 207.546 63.2295 199.115C55.8239 190.571 50.1843 180.601 46.3106 169.208C42.4369 157.701 40.5 145.226 40.5 131.781ZM89.7188 128.193V131.781C89.7188 139.529 90.4024 146.764 91.7696 153.486C93.1368 160.208 95.3015 166.132 98.2637 171.259C101.34 176.272 105.328 180.203 110.227 183.051C115.126 185.899 121.107 187.323 128.171 187.323C135.007 187.323 140.874 185.899 145.773 183.051C150.673 180.203 154.603 176.272 157.565 171.259C160.528 166.132 162.692 160.208 164.06 153.486C165.541 146.764 166.281 139.529 166.281 131.781V128.193C166.281 120.673 165.541 113.609 164.06 107.001C162.692 100.279 160.471 94.3547 157.395 89.2278C154.432 83.9869 150.502 79.8853 145.603 76.9231C140.703 73.9609 134.779 72.4797 127.829 72.4797C120.879 72.4797 114.955 73.9609 110.056 76.9231C105.271 79.8853 101.34 83.9869 98.2637 89.2278C95.3015 94.3547 93.1368 100.279 91.7696 107.001C90.4024 113.609 89.7188 120.673 89.7188 128.193Z" fill="white"/>
</mask>
<g mask="url(#mask0_30_31)">
<path d="M17.7519 90.6278C32.2336 84.5548 31.0535 58.0568 38.3066 62.5986C47.3731 68.2758 38.3066 41.1095 50.9198 47.1825C71.1339 56.9152 40.7009 64.5389 62.3666 76.9778C84.0323 89.4167 171.756 99.642 202.839 160.802C214.766 184.27 220.843 191.199 223.166 192.958C224.57 192.837 224.823 194.214 223.166 192.958C222.812 192.989 222.385 193.115 221.898 193.401C216.763 196.422 122.406 273.336 83.6205 225.168C44.8346 177 2.09417 132.401 13.321 120.526C22.3025 111.025 14.6184 97.1566 17.7519 90.6278Z" fill="url(#paint0_linear_30_31)"/>
<path d="M39.2409 66.8029C31.9877 62.2612 38.8572 79.7789 16.8175 87.8247C13.684 94.3536 21.1274 106.353 12.1459 115.854C0.919109 127.73 47.1703 180.27 85.9563 228.438C124.742 276.606 200.88 205.765 206.015 202.745C211.149 199.724 219.133 212.629 196.599 168.158C165.523 106.828 78.8569 98.1563 57.3454 76.3867C38.7095 57.5272 88.9205 36.7794 67.2701 41.1095C39.2409 46.7153 48.3073 72.4802 39.2409 66.8029Z" fill="url(#paint1_linear_30_31)"/>
</g>
<defs>
<linearGradient id="paint0_linear_30_31" x1="87.4684" y1="-29.7758" x2="62.9476" y2="328.852" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF0000" stop-opacity="0.74"/>
<stop offset="1" stop-color="#FF0000" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_30_31" x1="82.3228" y1="-32.8735" x2="57.802" y2="325.754" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF005C" stop-opacity="0.74"/>
<stop offset="1" stop-color="#FF005C" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

+17 -13
View File
@@ -1,27 +1,31 @@
export const STATIC_PORT = 4001;
// REST stuff
export const PROJECT_DATA = ['project'];
export const EVENTDATA_TABLE = ['eventdata'];
export const ALIASES = ['aliases'];
export const USERFIELDS = ['userFields'];
export const RUNDOWN = ['rundown'];
export const RUNDOWN_TABLE_KEY = 'rundown';
export const RUNDOWN_TABLE = [RUNDOWN_TABLE_KEY];
export const APP_INFO = ['appinfo'];
export const OSC_SETTINGS = ['oscSettings'];
export const HTTP_SETTINGS = ['httpSettings'];
export const APP_SETTINGS = ['appSettings'];
export const VIEW_SETTINGS = ['viewSettings'];
export const RUNTIME = ['runtimeStore'];
export const SHEET_STATE = ['sheetState'];
const location = window.location;
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
export const isProduction = import.meta.env.MODE === 'production';
// external stuff
export const githubURL = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
const STATIC_PORT = 4001;
export const serverPort = isProduction ? location.port : STATIC_PORT;
export const serverURL = `${location.protocol}//${location.hostname}:${serverPort}`;
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
/**
* @description finds server path given the current location, it
* @return {*}
*/
export const calculateServer = () => (import.meta.env.DEV ? `http://localhost:${STATIC_PORT}` : window.location.origin);
export const projectDataURL = `${serverURL}/project`;
export const rundownURL = `${serverURL}/events`;
export const serverURL = calculateServer();
export const websocketUrl = `ws://${window.location.hostname}:${STATIC_PORT}/ws`;
export const eventURL = `${serverURL}/eventdata`;
export const rundownURL = `${serverURL}/eventlist`;
export const ontimeURL = `${serverURL}/ontime`;
export const stylesPath = 'external/styles/override.css';
-53
View File
@@ -1,53 +0,0 @@
import axios, { AxiosError } from 'axios';
import { LogLevel } from 'ontime-types';
import { generateId, millisToString } from 'ontime-utils';
import { ontimeQueryClient } from '../queryClient';
import { addLog } from '../stores/logger';
import { nowInMillis } from '../utils/time';
export function maybeAxiosError(error: unknown) {
if (axios.isAxiosError(error)) {
const statusText = (error as AxiosError).response?.statusText ?? '';
let data = (error as AxiosError).response?.data ?? '';
if (typeof data === 'object') {
if ('message' in data) {
data = JSON.stringify(data.message);
} else {
data = JSON.stringify(data);
}
}
return `${statusText}: ${data}`;
} else {
if (typeof error !== 'string') {
return JSON.stringify(error);
}
return error;
}
}
export function logAxiosError(prepend: string, error: unknown) {
const message = `${prepend}: ${maybeAxiosError(error)}`;
addLog({
id: generateId(),
origin: 'SERVER',
time: millisToString(nowInMillis()),
level: LogLevel.Error,
text: message,
});
}
/**
* Utility function invalidates react-query caches
*/
export async function invalidateAllCaches() {
await ontimeQueryClient.invalidateQueries({ queryKey: ['project'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['aliases'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['userFields'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['rundown'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['appinfo'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['oscSettings'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['appSettings'] });
await ontimeQueryClient.invalidateQueries({ queryKey: ['viewSettings'] });
}
@@ -0,0 +1,21 @@
import axios from 'axios';
import { EventData } from 'ontime-types';
import { eventURL } from './apiConstants';
/**
* @description HTTP request to fetch event data
* @return {Promise}
*/
export async function fetchEventData(): Promise<EventData> {
const res = await axios.get(eventURL);
return res.data;
}
/**
* @description HTTP request to mutate event data
* @return {Promise}
*/
export async function postEventData(data: EventData) {
return axios.post(eventURL, data);
}
+9 -24
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import { GetRundownCached, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
import { OntimeRundown, OntimeRundownEntry } from 'ontime-types';
import { rundownURL } from './apiConstants';
@@ -7,16 +7,6 @@ import { rundownURL } from './apiConstants';
* @description HTTP request to fetch all events
* @return {Promise}
*/
export async function fetchCachedRundown(): Promise<GetRundownCached> {
const res = await axios.get(`${rundownURL}/cached`);
return res.data;
}
/**
* @deprecated use fetchCachedRundown instead
* @description HTTP request to fetch all events
* @return {Promise}
*/
export async function fetchRundown(): Promise<OntimeRundown> {
const res = await axios.get(rundownURL);
return res.data;
@@ -38,6 +28,14 @@ export async function requestPutEvent(data: Partial<OntimeRundownEntry>) {
return axios.put(rundownURL, data);
}
/**
* @description HTTP request to modify event
* @return {Promise}
*/
export async function requestPatchEvent(data: OntimeRundownEntry) {
return axios.patch(rundownURL, data);
}
export type ReorderEntry = {
eventId: string;
from: number;
@@ -60,19 +58,6 @@ export async function requestApplyDelay(eventId: string) {
return axios.patch(`${rundownURL}/applydelay/${eventId}`);
}
export type SwapEntry = {
from: string;
to: string;
};
/**
* @description HTTP request to swap two events
* @return {Promise}
*/
export async function requestEventSwap(data: SwapEntry) {
return axios.patch(`${rundownURL}/swap`, data);
}
/**
* @description HTTP request to delete given event
* @return {Promise}
+36 -194
View File
@@ -1,23 +1,9 @@
import axios, { AxiosResponse } from 'axios';
import {
Alias,
DatabaseModel,
GetInfo,
HttpSettings,
OntimeRundown,
OSCSettings,
OscSubscription,
ProjectData,
Settings,
UserFields,
ViewSettings,
} from 'ontime-types';
import { ExcelImportMap } from 'ontime-utils';
import axios from 'axios';
import { Alias, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types';
import { apiRepoLatest } from '../../externals';
import fileDownload from '../utils/fileDownload';
import { InfoType } from '../models/Info';
import { ontimeURL } from './apiConstants';
import { githubURL, ontimeURL } from './apiConstants';
/**
* @description HTTP request to retrieve application settings
@@ -40,7 +26,7 @@ export async function postSettings(data: Settings) {
* @description HTTP request to retrieve application info
* @return {Promise}
*/
export async function getInfo(): Promise<GetInfo> {
export async function getInfo(): Promise<InfoType> {
const res = await axios.get(`${ontimeURL}/info`);
return res.data;
}
@@ -58,7 +44,7 @@ export async function getView(): Promise<ViewSettings> {
* @description HTTP request to mutate view settings
* @return {Promise}
*/
export async function postViewSettings(data: ViewSettings) {
export async function postView(data: ViewSettings) {
return axios.post(`${ontimeURL}/views`, data);
}
@@ -105,23 +91,6 @@ export async function getOSC(): Promise<OSCSettings> {
return res.data;
}
/**
* @description HTTP request to retrieve http settings
* @return {Promise}
*/
export async function getHTTP(): Promise<HttpSettings> {
const res = await axios.get(`${ontimeURL}/http`);
return res.data;
}
/**
* @description HTTP request to mutate http settings
* @return {Promise}
*/
export async function postHTTP(data: HttpSettings) {
return axios.post(`${ontimeURL}/http`, data);
}
/**
* @description HTTP request to mutate osc settings
* @return {Promise}
@@ -131,46 +100,45 @@ export async function postOSC(data: OSCSettings) {
}
/**
* @description HTTP request to mutate osc subscriptions
* @description HTTP request to download db
* @return {Promise}
*/
export async function postOscSubscriptions(data: OscSubscription) {
return axios.post(`${ontimeURL}/osc-subscriptions`, data);
}
export const downloadRundown = async () => {
await axios({
url: `${ontimeURL}/db`,
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
const headerLine = response.headers['Content-Disposition'];
let filename = 'rundown.json';
/**
* @description HTTP request to download db in CSV format
*/
export const downloadCSV = () => {
return fileDownload(ontimeURL, { name: 'rundown', type: 'csv' }, { type: 'text/csv;charset=utf-8;' });
};
// try and get the filename from the response
if (headerLine != null) {
const startFileNameIndex = headerLine.indexOf('"') + 1;
const endFileNameIndex = headerLine.lastIndexOf('"');
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
/**
* @description HTTP request to download db in JSON format
*/
export const downloadRundown = () => {
return fileDownload(ontimeURL, { name: 'rundown', type: 'json' }, { type: 'application/json;charset=utf-8;' });
};
// TODO: should this be extracted to shared code?
export type ProjectFileImportOptions = {
onlyRundown: boolean;
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
});
};
/**
* @description HTTP request to upload events db
* @return {Promise}
*/
export const uploadProjectFile = async (
file: File,
setProgress: (value: number) => void,
options?: Partial<ProjectFileImportOptions>,
) => {
type UploadDataOptions = {
onlyRundown?: boolean;
};
export const uploadData = async (file: string, setProgress: (value: number) => void, options?: UploadDataOptions) => {
const formData = new FormData();
formData.append('userFile', file);
const onlyRundown = Boolean(options?.onlyRundown);
const onlyRundown = options?.onlyRundown || 'false';
await axios
.post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, {
headers: {
@@ -184,137 +152,11 @@ export const uploadProjectFile = async (
.then((response) => response.data.id);
};
/**
* @description Make patch changes to the objects in the db
* @return {Promise}
*/
export async function patchData(patchDb: Partial<DatabaseModel>) {
const response = await axios.patch(`${ontimeURL}/db`, patchDb);
return response;
}
type PostPreviewExcelResponse = {
rundown: OntimeRundown;
project: ProjectData;
userFields: UserFields;
};
/**
* @description Make patch changes to the objects in the db
* @return {Promise} - returns parsed rundown and userfields
*/
export async function postPreviewExcel(file: File, setProgress: (value: number) => void, options?: ExcelImportMap) {
const formData = new FormData();
formData.append('userFile', file);
formData.append('options', JSON.stringify(options));
const response: AxiosResponse<PostPreviewExcelResponse> = await axios.post(
`${ontimeURL}/preview-spreadsheet`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
const complete = progressEvent?.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0;
setProgress(complete);
},
},
);
return response;
}
export type HasUpdate = {
url: string;
version: string;
};
/**
* @description HTTP request to get the latest version and url from github
* @return {Promise}
*/
export async function getLatestVersion(): Promise<HasUpdate> {
const res = await axios.get(`${apiRepoLatest}`);
return {
url: res.data.html_url as string,
version: res.data.tag_name as string,
};
export async function getLatestVersion(): Promise<object> {
const res = await axios.get(`${githubURL}`);
return { url: res.data.html_url, version: res.data.tag_name };
}
export async function postNew(initialData: Partial<ProjectData>) {
return axios.post(`${ontimeURL}/new`, initialData);
}
/**
* @description STEP 1
*/
export const uploadSheetClientFile = async (file: File) => {
const formData = new FormData();
formData.append('userFile', file);
const res = await axios
.post(`${ontimeURL}/sheet/clientsecret`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then((response) => response.data.id);
return res;
};
/**
* @description STEP 1 test
*/
export const getClientSecrect = async () => {
const response = await axios.get(`${ontimeURL}/sheet/clientsecret`);
return response.data;
};
/**
* @description STEP 2
*/
export const getSheetsAuthUrl = async () => {
const response = await axios.get(`${ontimeURL}/sheet/authentication/url`);
return response.data;
};
/**
* @description STEP 2 test
*/
export const getAuthentication = async () => {
const response = await axios.get(`${ontimeURL}/sheet/authentication`);
return response.data;
};
/**
* @description STEP 3
* @returns worksheetOptions
*/
export const postId = async (id: string) => {
const response = await axios.post(`${ontimeURL}/sheet/id`, { id });
return response.data;
};
/**
* @description STEP 4
*/
export const postWorksheet = async (id: string, worksheet: string) => {
const response = await axios.post(`${ontimeURL}/sheet/worksheet`, { id, worksheet });
return response.data;
};
/**
* @description STEP 5
*/
export const postPreviewSheet = async (id: string, options: ExcelImportMap) => {
const response = await axios.post(`${ontimeURL}/sheet/pull`, { id, options });
return response.data.data;
};
/**
* @description STEP 5
*/
export const postPushSheet = async (id: string, options: ExcelImportMap) => {
const response = await axios.post(`${ontimeURL}/sheet-push`, { id, options });
return response.data.data;
};
@@ -1,21 +0,0 @@
import axios from 'axios';
import { ProjectData } from 'ontime-types';
import { projectDataURL } from './apiConstants';
/**
* @description HTTP request to fetch project data
* @return {Promise}
*/
export async function getProjectData(): Promise<ProjectData> {
const res = await axios.get(projectDataURL);
return res.data;
}
/**
* @description HTTP request to mutate project data
* @return {Promise}
*/
export async function postProjectData(data: ProjectData) {
return axios.post(projectDataURL, data);
}
@@ -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']),
};
@@ -18,7 +18,6 @@ import { useEmitLog } from '../../stores/logger';
interface QuitIconBtnProps {
clickHandler: () => void;
size?: Size;
disabled?: boolean;
}
const quitBtnStyle = {
@@ -28,10 +27,6 @@ const quitBtnStyle = {
_hover: {
background: '#D20300', // $red-700
color: 'white',
_disabled: {
color: '#D20300', // $red-700
background: 'none',
},
},
_active: {
background: '#9A0000', // $red-1000
@@ -42,7 +37,7 @@ const quitBtnStyle = {
};
export default function QuitIconBtn(props: QuitIconBtnProps) {
const { clickHandler, size = 'lg', disabled, ...rest } = props;
const { clickHandler, size = 'lg', ...rest } = props;
const [isOpen, setIsOpen] = useState(false);
const { emitInfo } = useEmitLog();
const onClose = () => setIsOpen(false);
@@ -70,7 +65,6 @@ export default function QuitIconBtn(props: QuitIconBtnProps) {
size={size}
icon={<FiPower />}
onClick={() => setIsOpen(true)}
isDisabled={disabled}
{...quitBtnStyle}
{...rest}
/>
@@ -81,7 +75,9 @@ export default function QuitIconBtn(props: QuitIconBtnProps) {
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
Ontime Shutdown
</AlertDialogHeader>
<AlertDialogBody>This will shutdown the program and all running servers. Are you sure?</AlertDialogBody>
<AlertDialogBody>
This will shutdown the program and all running servers. Are you sure?
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose} variant='ghost'>
Cancel
@@ -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
}
@@ -1,8 +1,7 @@
import { MouseEvent } from 'react';
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
interface TooltipActionBtnProps extends IconButtonProps {
clickHandler: (event?: MouseEvent) => void | Promise<void>;
clickHandler: () => void;
tooltip: string;
openDelay?: number;
}
@@ -11,7 +10,13 @@ export default function TooltipActionBtn(props: TooltipActionBtnProps) {
const { clickHandler, icon, size = 'xs', tooltip, openDelay = 0, className, ...rest } = props;
return (
<Tooltip label={tooltip} openDelay={openDelay}>
<IconButton {...rest} size={size} icon={icon} onClick={clickHandler} className={className} />
<IconButton
{...rest}
size={size}
icon={icon}
onClick={clickHandler}
className={className}
/>
</Tooltip>
);
}
@@ -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,
};
@@ -6,7 +6,6 @@
display: flex;
justify-content: space-between;
color: $section-white;
margin-top: $main-spacing;
border-bottom: 1px solid $border-color-ondark;
padding-bottom: $element-inner-spacing;
margin-bottom: $element-spacing;
@@ -1,9 +0,0 @@
.contextMenuButton {
position: absolute;
cursor: pointer;
}
.contextMenuBackdrop {
position: fixed;
inset: 0;
}
@@ -1,84 +0,0 @@
// logic (with some modifications) culled from:
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
import { Fragment, ReactElement } from 'react';
import { Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react';
import { IconType } from '@react-icons/all-files';
import { create } from 'zustand';
import style from './ContextMenu.module.scss';
type ContextMenuCoords = {
x: number;
y: number;
};
export type Option = {
label: string;
icon: IconType;
onClick: () => void;
withDivider?: boolean;
isDisabled?: boolean;
};
type ContextMenuStore = {
coords: ContextMenuCoords;
options: Option[];
isOpen: boolean;
setContextMenu: (coords: ContextMenuCoords, options: Option[]) => void;
setIsOpen: (newIsOpen: boolean) => void;
};
export const useContextMenuStore = create<ContextMenuStore>((set) => ({
coords: { x: 0, y: 0 },
options: [],
isOpen: false,
setContextMenu: (coords, options) => set(() => ({ coords, options, isOpen: true })),
setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })),
}));
interface ContextMenuProps {
// ReactElement type required due to early `return` (line 51) returning {children}
children: ReactElement;
}
export const ContextMenu = ({ children }: ContextMenuProps) => {
const { coords, options, isOpen, setIsOpen } = useContextMenuStore();
const onClose = () => {
return setIsOpen(false);
};
if (!isOpen) {
return children;
}
return (
<>
{children}
<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, withDivider, isDisabled }, i) => (
<Fragment key={label}>
{withDivider && <MenuDivider />}
<MenuItem key={i} icon={<Icon />} onClick={onClick} isDisabled={isDisabled}>
{label}
</MenuItem>
</Fragment>
))}
</MenuList>
</Menu>
</>
);
};
@@ -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,15 +14,21 @@ interface CopyTagProps {
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
const { label, className, size = 'xs', children } = props;
const handleClick = () => copyToClipboard(children as string);
return (
<Tooltip label={label} openDelay={tooltipDelayFast}>
<ButtonGroup size={size} isAttached className={className}>
<Button variant='ontime-subtle' tabIndex={-1}>
{children}
</Button>
<IconButton aria-label={label} icon={<IoCopy />} variant='ontime-filled' tabIndex={-1} onClick={handleClick} />
<ButtonGroup
size={size}
isAttached
className={className}
>
<Button variant='ontime-subtle' tabIndex={-1}>{children}</Button>
<IconButton
aria-label={label}
icon={<IoCopy />}
variant='ontime-filled'
tabIndex={-1}
onClick={() => navigator.clipboard.writeText(children as string)}
/>
</ButtonGroup>
</Tooltip>
);
@@ -1,9 +0,0 @@
@use '../../../theme/v2Styles' as *;
.delaySymbol {
svg {
font-size: 1.5rem;
color: $ontime-delay;
margin: 0 auto;
}
}
@@ -1,40 +0,0 @@
import { Tooltip } from '@chakra-ui/react';
import { IoChevronDown } from '@react-icons/all-files/io5/IoChevronDown';
import { IoChevronUp } from '@react-icons/all-files/io5/IoChevronUp';
import { tooltipDelayFast } from '../../../ontimeConfig';
import { millisToDelayString } from '../../utils/dateConfig';
import style from './DelayIndicator.module.scss';
interface DelayIndicatorProps {
delayValue?: number;
}
export default function DelayIndicator(props: DelayIndicatorProps) {
const { delayValue } = props;
if (typeof delayValue === 'number') {
if (delayValue < 0) {
return (
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
<span className={style.delaySymbol}>
<IoChevronDown />
</span>
</Tooltip>
);
}
if (delayValue > 0) {
return (
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
<span className={style.delaySymbol}>
<IoChevronUp />
</span>
</Tooltip>
);
}
}
return null;
}
@@ -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,12 +24,16 @@ 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 });
});
try {
this.context.emitError(error.toString());
} catch (e) {
Sentry.captureMessage(`Unable to emit error ${error} ${e}`);
}
this.reportContent = `${error} ${info.componentStack}`;
}
@@ -3,7 +3,12 @@ import { Textarea, TextareaProps } from '@chakra-ui/react';
// @ts-expect-error no types from library
import autosize from 'autosize/dist/autosize';
export const AutoTextArea = (props: TextareaProps) => {
interface AutoTextAreaProps extends TextareaProps {
isDark?: boolean;
}
export const AutoTextArea = (props: AutoTextAreaProps) => {
const { isDark, ...rest } = props;
const ref = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
@@ -14,6 +19,7 @@ export const AutoTextArea = (props: TextareaProps) => {
};
}, []);
return (
<Textarea
overflow='hidden'
@@ -21,8 +27,8 @@ export const AutoTextArea = (props: TextareaProps) => {
resize='none'
ref={ref}
transition='height none'
variant='ontime-transparent'
{...props}
variant={isDark ? 'ontime-filled' : 'ontime-filled-onlight'}
{...rest}
/>
);
};
@@ -0,0 +1,7 @@
input[type="color"] {
appearance: none;
cursor: pointer;
height: 32px;
width: 32px;
padding: 0;
}
@@ -0,0 +1,25 @@
import { Input } from '@chakra-ui/react';
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
import style from './ColourInput.module.scss';
interface ColourInputProps {
value: string;
name: EventEditorSubmitActions;
handleChange: (newValue: EventEditorSubmitActions, name: string) => void;
}
export default function ColourInput(props: ColourInputProps) {
const { value, name, handleChange } = props;
return (
<Input
size='sm'
variant='ontime-filled'
className={style.colourInput}
type='color'
value={value}
onChange={(event) => handleChange(name, event.target.value)}
/>
);
}
@@ -1,26 +0,0 @@
import { IoBan } from '@react-icons/all-files/io5/IoBan';
import { cx } from '../../../utils/styleUtils';
import style from './SwatchSelect.module.scss';
interface SwatchProps {
color: string;
onClick: (color: string) => void;
isSelected?: boolean;
}
export default function Swatch(props: SwatchProps) {
const { color, isSelected, onClick } = props;
const classes = cx([style.swatch, isSelected ? style.selected : null]);
if (!color) {
return (
<div className={`${classes} ${style.center}`} onClick={() => onClick('')}>
<IoBan />
</div>
);
}
return <div className={classes} style={{ backgroundColor: `${color}` }} onClick={() => onClick(color)} />;
}
@@ -1,24 +0,0 @@
.list {
display: flex;
gap: 4px;
}
.swatch {
cursor: pointer;
aspect-ratio: 1;
width: 2rem;
height: 2rem;
border-radius: 16px;
border: 4px solid #262626;
&.selected {
border: 2px solid #578AF4;
}
}
.center {
display: grid;
place-content: center;
color: #578AF4;
}
@@ -1,50 +0,0 @@
import { useCallback } from 'react';
import { EditorUpdateFields } from '../../../../features/event-editor/EventEditor';
import Swatch from './Swatch';
import style from './SwatchSelect.module.scss';
interface ColourInputProps {
value: string;
name: EditorUpdateFields;
handleChange: (newValue: EditorUpdateFields, name: string) => void;
}
const colours = [
'',
'#FFCC78', // $orange-400
'#FFAB33', // $orange-600
'#77C785', // $green-400
'#339E4E', // $green-600
'#779BE7', // $blue-400
'#3E75E8', // $blue-600
'#FF7878', // $red-400
'#ED3333', // $red-600
'#A790F5', // $violet-400
'#8064E1', // $violet-600
'#9d9d9d', // $gray-500
'#ececec', // $gray-100
];
export default function SwatchSelect(props: ColourInputProps) {
const { value, name, handleChange } = props;
const setColour = useCallback(
(newValue: string) => {
if (newValue !== value) {
handleChange(name, newValue);
}
},
[handleChange, name, value],
);
return (
<div className={style.list}>
{colours.map((colour) => (
<Swatch key={colour} color={colour} onClick={setColour} isSelected={value === colour} />
))}
</div>
);
}
@@ -1,24 +1,13 @@
@use '../../../../theme/v2Styles' as *;
$input-font-size: 15px;
.delayInput {
display: flex;
gap: $element-spacing;
align-items: center;
color: $ontime-delay-text;
font-size: $text-body-size;
.inputField {
font-size: $input-font-size;
letter-spacing: 1px;
max-width: 7em;
padding-left: 16px;
color: $ontime-delay-text
}
}
.delayOptions {
display: flex;
flex-direction: column;
}
.inputField {
text-align: center;
}
@@ -1,134 +1,82 @@
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
import { Input, Radio, RadioGroup } from '@chakra-ui/react';
import { millisToString } from 'ontime-utils';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Input } from '@chakra-ui/react';
import { useEventAction } from '../../../hooks/useEventAction';
import { forgivingStringToMillis } from '../../../utils/dateConfig';
import { clamp } from '../../../utils/math';
import style from './DelayInput.module.scss';
const inputStyleProps = {
width: 20,
placeholder: '-',
size: 'sm',
color: '#E69056',
variant: 'ontime-filled',
};
interface DelayInputProps {
eventId: string;
duration: number;
submitHandler: (value: number) => void;
value?: number;
}
export default function DelayInput(props: DelayInputProps) {
const { eventId, duration } = props;
const { updateEvent } = useEventAction();
const [value, setValue] = useState<string>('');
const { submitHandler, value = 0 } = props;
const [_value, setValue] = useState(value);
const inputRef = useRef<HTMLInputElement | null>(null);
// avoid wrong submit on cancel
let ignoreChange = false;
useEffect(() => {
if (typeof duration === 'undefined') {
return;
}
setValue(millisToString(duration));
}, [duration]);
if (value == null) return;
setValue(value);
}, [value]);
/**
* @description Prepare delay value for update
* @param {string} newValue string to be parsed
* @param {string} value string to be parsed
*/
const validateAndSubmit = (newValue: string) => {
if (ignoreChange) {
ignoreChange = false;
return;
}
const validate = useCallback(
(newValue?: string) => {
if (newValue === '') setValue(0);
const delayValue = clamp(Number(newValue), -60, 60);
const isNegative = newValue.startsWith('-');
let newMillis = forgivingStringToMillis(newValue);
if (delayValue === value) return;
setValue(delayValue);
if (isNegative) {
newMillis = newMillis * -1;
}
if (newMillis === duration) {
return;
}
submitChange(newMillis);
setValue(millisToString(newMillis));
};
const submitChange = (value: number) => {
updateEvent({
id: eventId,
duration: value,
});
};
/**
* @description Selects input text on focus
*/
const handleFocus = () => inputRef.current?.select();
submitHandler(delayValue);
},
[submitHandler, value],
);
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
const onKeyDownHandler = useCallback((key: string) => {
if (key === 'Enter') {
inputRef.current?.blur();
validateAndSubmit((event.target as HTMLInputElement).value);
} else if (event.key === 'Tab') {
validateAndSubmit((event.target as HTMLInputElement).value);
} else if (event.key === 'Escape') {
ignoreChange = true;
setValue(millisToString(duration));
validate(inputRef.current?.value);
} else if (key === 'Escape') {
inputRef.current?.blur();
setValue(value);
}
};
}, [validate, value]);
/**
* @description handles direction change to delay
* @param newDirection
*/
const handleSlipChange = (newDirection: 'add' | 'subtract') => {
if (newDirection === 'add') {
// add time
if (duration < 0) {
submitChange(duration * -1);
}
} else if (newDirection === 'subtract') {
// subtract time
if (duration > 0) {
submitChange(duration * -1);
}
}
};
const checkedOption = value.startsWith('-') ? 'subtract' : 'add';
const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
value !== undefined && value >= 0 ? 'delayed' : 'ahead'
}`;
return (
<div className={style.delayInput}>
<label className={style.delayInput}>
<Input
size='sm'
{...inputStyleProps}
ref={inputRef}
data-testid='delay-input'
className={style.inputField}
type='text'
placeholder='-'
variant='ontime-filled'
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={(event) => validateAndSubmit(event.target.value)}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={9}
value={_value}
onChange={(event) => setValue(Number(event.target.value))}
onBlur={(event) => validate(event.target.value)}
onKeyDown={(event) => onKeyDownHandler(event.key)}
type='number'
/>
<RadioGroup
className={style.delayOptions}
onChange={handleSlipChange}
value={checkedOption}
variant='ontime-block'
size='sm'
>
<Radio value='add'>Add time</Radio>
<Radio value='subtract'>Subtract time</Radio>
</RadioGroup>
</div>
{labelText}
</label>
);
}
@@ -1,19 +0,0 @@
@use "../../../../theme/_ontimeColours" as *;
@use "../../../../theme/_v2Styles" as *;
.swatch {
width: 24px;
height: 24px;
border-radius: 12px;
border: 1px solid white;
box-shadow: 0 0 0 1px $gray-300;
cursor: pointer;
transition-property: box-shadow;
transition-duration: $transition-time-action;
}
.swatch:hover {
box-shadow: 0 0 0 1px $blue-300;
}
@@ -1,35 +0,0 @@
import { HexAlphaColorPicker } from 'react-colorful';
import { useController, UseControllerProps } from 'react-hook-form';
import { Popover, PopoverContent, PopoverTrigger } from '@chakra-ui/react';
import { ViewSettings } from 'ontime-types';
import style from './PopoverPicker.module.scss';
export function PopoverPickerRHF(props: UseControllerProps<ViewSettings>) {
const { name, control } = props;
const {
field: { onChange, value },
} = useController({ control, name });
return <PopoverPicker color={value as string} onChange={onChange} />;
}
interface PopoverPickerProps {
color: string;
onChange: (color: string) => void;
}
export default function PopoverPicker(props: PopoverPickerProps) {
const { color, onChange } = props;
return (
<Popover>
<PopoverTrigger>
<div className={style.swatch} style={{ backgroundColor: color }} />
</PopoverTrigger>
<PopoverContent style={{ width: 'auto' }}>
<HexAlphaColorPicker color={color} onChange={onChange} />
</PopoverContent>
</Popover>
);
}
@@ -15,21 +15,15 @@ interface BaseProps {
submitHandler: (field: EventEditorSubmitActions, newValue: string) => void;
}
interface TextInputProps extends BaseProps {
isTextArea?: false;
}
type ResizeOptions = 'horizontal' | 'vertical' | 'none';
interface TextAreaProps extends BaseProps {
interface TextAreaProps {
isTextArea: true;
resize?: ResizeOptions;
resize?: 'horizontal' | 'vertical' | 'none';
}
type InputProps = TextInputProps | TextAreaProps;
type TextInputProps = BaseProps & TextAreaProps;
export default function TextInput(props: InputProps) {
const { isTextArea, isFullHeight, size = 'sm', field, initialText = '', submitHandler } = props;
export default function TextInput(props: TextInputProps) {
const { isTextArea, isFullHeight, size = 'sm', field, initialText = '', submitHandler, resize = 'none' } = props;
const inputRef = useRef(null);
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
@@ -37,11 +31,6 @@ export default function TextInput(props: InputProps) {
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
let resize: ResizeOptions = 'none';
if (isTextArea) {
resize = (props as TextAreaProps)?.resize ?? 'none';
}
return isTextArea ? (
<Textarea
ref={inputRef}
@@ -1,4 +1,4 @@
import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react';
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
interface UseReactiveTextInputReturn {
value: string;
@@ -14,7 +14,8 @@ export default function useReactiveTextInput(
submitOnEnter?: boolean;
},
): UseReactiveTextInputReturn {
const [text, setText] = useState<string>(initialText);
const [text, setText] = useState(initialText);
useEffect(() => {
if (typeof initialText === 'undefined') {
@@ -57,6 +58,7 @@ export default function useReactiveTextInput(
[initialText, submitCallback],
);
/**
* @description Handles common keys for submit and cancel
* @param {string} key
@@ -79,8 +81,8 @@ export default function useReactiveTextInput(
return {
value: text,
onChange: (event: ChangeEvent) => handleChange((event.target as HTMLInputElement).value),
onBlur: (event: ChangeEvent) => handleSubmit((event.target as HTMLInputElement).value),
onKeyDown: (event: KeyboardEvent) => keyHandler(event.key),
onChange: (event) => handleChange((event.target as HTMLInputElement).value),
onBlur: (event) => handleSubmit((event.target as HTMLInputElement).value),
onKeyDown: (event) => keyHandler(event.key),
};
}
@@ -1,16 +1,9 @@
@use "../../../../theme/v2Styles" as *;
$input-font-size: 15px;
$input-delayed-border-color: #E69056;
.timeInput {
width: fit-content !important;
.inputLeft {
max-width: fit-content;
}
.inputLeft,
.inputButton {
aspect-ratio: 1;
}
@@ -22,13 +15,6 @@ $input-delayed-border-color: #E69056;
padding: 0 0 0 2.6em;
}
.warn {
&::after {
content: "*";
color: $warning-orange;
}
}
&.delayed {
.inputField {
border: 1px solid $input-delayed-border-color;
@@ -1,58 +1,42 @@
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
import { millisToString } from 'ontime-utils';
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
import { tooltipDelayFast } from '../../../../ontimeConfig';
import { useEmitLog } from '../../../stores/logger';
import { forgivingStringToMillis } from '../../../utils/dateConfig';
import { cx } from '../../../utils/styleUtils';
import { TimeEntryField } from '../../../utils/timesManager';
import style from './TimeInput.module.scss';
interface TimeInputProps {
id?: TimeEntryField;
name: TimeEntryField;
submitHandler: (field: TimeEntryField, value: number) => void;
submitHandler: (field: EventEditorSubmitActions, value: number) => void;
time?: number;
delay?: number;
placeholder: string;
validationHandler: (entry: TimeEntryField, val: number) => boolean;
previousEnd?: number;
tooltip?: string;
}
function ButtonInitial(name: TimeEntryField) {
if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E';
if (name === 'durationOverride') return 'D';
return '';
}
function ButtonTooltip(name: TimeEntryField, tooltip?: string) {
if (name === 'timeStart') return `Start${tooltip ? `: ${tooltip}` : ''}`;
if (name === 'timeEnd') return `End${tooltip ? `: ${tooltip}` : ''}`;
if (name === 'durationOverride') return `Duration${tooltip ? `: ${tooltip}` : ''}`;
return '';
}
export default function TimeInput(props: TimeInputProps) {
const { id, name, submitHandler, time = 0, delay = 0, placeholder, previousEnd = 0 } = props;
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0 } = props;
const { emitError } = useEmitLog();
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState<string>('');
const ignoreChange = useRef(false);
const [value, setValue] = useState('');
/**
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
// Todo: check if change is necessary
try {
setValue(millisToString(time));
setValue(millisToString(time + delay));
} catch (error) {
setValue(millisToString(0));
emitError(`Unable to parse time ${time}: ${error}`);
emitError(`Unable to parse date: ${error}`);
}
}, [emitError, time]);
}, [delay, emitError, time]);
/**
* @description Selects input text on focus
@@ -89,15 +73,21 @@ export default function TimeInput(props: TimeInputProps) {
newValMillis = forgivingStringToMillis(newValue);
}
// Time now and time submittedVal
const originalMillis = time + delay;
// check if time is different from before
if (newValMillis === time) return false;
if (newValMillis === originalMillis) return false;
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
// update entry
submitHandler(name, newValMillis);
return true;
},
[name, previousEnd, submitHandler, time],
[delay, name, previousEnd, submitHandler, time, validationHandler],
);
/**
@@ -109,13 +99,12 @@ export default function TimeInput(props: TimeInputProps) {
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
const delayed = name === 'timeEnd' ? Math.max(0, ms + delay) : Math.max(0, ms + delay);
setValue(millisToString(delayed));
setValue(millisToString(ms + delay));
} else {
resetValue();
}
},
[delay, handleSubmit, name, resetValue],
[delay, handleSubmit, resetValue],
);
/**
@@ -131,7 +120,6 @@ export default function TimeInput(props: TimeInputProps) {
validateAndSubmit((event.target as HTMLInputElement).value);
}
if (event.key === 'Escape') {
ignoreChange.current = true;
inputRef.current?.blur();
resetValue();
}
@@ -141,10 +129,6 @@ export default function TimeInput(props: TimeInputProps) {
const onBlurHandler = useCallback(
(event: FocusEvent<HTMLInputElement>) => {
if (ignoreChange.current) {
ignoreChange.current = false;
return;
}
validateAndSubmit((event.target as HTMLInputElement).value);
},
[validateAndSubmit],
@@ -153,41 +137,44 @@ export default function TimeInput(props: TimeInputProps) {
useEffect(() => {
if (time == null) return;
resetValue();
}, [resetValue, time]);
}, [emitError, resetValue, time]);
const isDelayed = delay !== 0;
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null]);
const isDelayed = delay != null && delay !== 0;
const TooltipLabel = useMemo(() => {
return ButtonTooltip(name, '');
}, [name]);
const ButtonInitial = () => {
if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E';
if (name === 'durationOverride') return 'D';
return '';
};
const ButtonText = useMemo(() => {
return ButtonInitial(name);
}, [name]);
const ButtonTooltip = () => {
if (name === 'timeStart') return 'Start';
if (name === 'timeEnd') return 'End';
if (name === 'durationOverride') return 'Duration';
return '';
};
return (
<InputGroup size='sm' className={inputClasses}>
<InputLeftElement className={style.inputLeft}>
<Tooltip label={TooltipLabel} openDelay={tooltipDelayFast} variant='ontime-ondark'>
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
<InputLeftElement width='fit-content'>
<Tooltip label={ButtonTooltip()} openDelay={tooltipDelayFast} variant='ontime-ondark'>
<Button
size='sm'
variant='ontime-subtle-white'
className={buttonClasses}
className={`${style.inputButton} ${isDelayed ? style.delayed : ''}`}
tabIndex={-1}
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
borderRight='1px solid transparent'
borderRadius='2px 0 0 2px'
>
{ButtonText}
{ButtonInitial()}
</Button>
</Tooltip>
</InputLeftElement>
<Input
ref={inputRef}
id={id}
data-testid={`time-input-${name}`}
data-testid='time-input'
className={style.inputField}
type='text'
placeholder={placeholder}
@@ -198,7 +185,6 @@ export default function TimeInput(props: TimeInputProps) {
onKeyDown={onKeyDownHandler}
value={value}
maxLength={8}
autoComplete='off'
/>
</InputGroup>
);
@@ -1,48 +0,0 @@
@use '../../../theme/viewerDefs' as *;
$progress-bar-size: 12px;
$progress-bar-br: 3px;
.multiprogress-bar {
position: relative;
width: 100%;
height: $progress-bar-size;
border-radius: $progress-bar-br;
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
display: flex;
overflow: hidden;
&--hidden {
display: none;
transition: display 0.5s;
}
}
.multiprogress-bar__indicator {
position: absolute;
height: inherit;
background-color: black;
opacity: 0.8;
transition: 1s linear;
transition-property: width;
right: 0;
}
.multiprogress-bar__bg-normal {
position: absolute;
height: inherit;
right: 0;
width: 100%;
}
.multiprogress-bar__bg-warning {
position: absolute;
height: inherit;
right: 0;
}
.multiprogress-bar__bg-danger {
position: absolute;
height: inherit;
right: 0;
}
@@ -1,43 +0,0 @@
import { clamp } from '../../utils/math';
import './MultiPartProgressBar.scss';
interface MultiPartProgressBar {
now: number | null;
complete: number;
normalColor: string;
warning: number;
warningColor: string;
danger: number;
dangerColor: string;
hidden?: boolean;
className?: string;
}
export default function MultiPartProgressBar(props: MultiPartProgressBar) {
const { now, complete, normalColor, warning, warningColor, danger, dangerColor, hidden, className = '' } = props;
const percentComplete = 100 - clamp(100 - (Math.max(now ?? 0, 0) * 100) / complete, 0, 100);
const dangerWidth = clamp((danger / complete) * 100, 0, 100);
const warningWidth = clamp((warning / complete) * 100, 0, 100);
return (
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
{now !== null && (
<>
<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>
);
}
@@ -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;
@@ -10,27 +10,18 @@ $icon-color: $ui-white;
$button-bg: $gray-1050;
$button-size: 48px;
.buttonContainer {
display: flex;
flex-direction: column;
row-gap: 1rem;
padding: 0.5em;
transition-property: opacity;
transition-duration: 0.3s;
opacity: 1;
position: fixed;
left: 0;
top: 0;
z-index: 2;
&.hidden {
opacity: 0;
}
.mirror {
transform: rotate(180deg);
}
.button {
.navButton {
z-index: 2;
position: absolute;
left: 0.5em;
top: 0.5em;
transition-property: opacity;
transition-duration: 0.3s;
opacity: 1;
font-size: 24px;
color: $icon-color;
background-color: $button-bg;
@@ -39,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,40 +1,33 @@
import { KeyboardEvent, memo, useEffect, useRef, useState } from 'react';
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Link, useLocation, useSearchParams } from 'react-router-dom';
import { useDisclosure } from '@chakra-ui/react';
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';
import useClickOutside from '../../hooks/useClickOutside';
import useFullscreen from '../../hooks/useFullscreen';
import { useKeyDown } from '../../hooks/useKeyDown';
import { useViewOptionsStore } from '../../stores/viewOptions';
import RenameClientModal from './rename-client-modal/RenameClientModal';
import style from './NavigationMenu.module.scss';
function NavigationMenu() {
export default function NavigationMenu() {
const location = useLocation();
const { isFullScreen, toggleFullScreen } = useFullscreen();
const { toggleMirror } = useViewOptionsStore();
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 { isOpen, onOpen, onClose } = useDisclosure();
const toggleMenu = () => setShowMenu((prev) => !prev);
useKeyDown(toggleMenu, ' ');
// show on mouse move
useEffect(() => {
let fadeOut: NodeJS.Timeout | null = null;
const setShowMenuTrue = () => {
@@ -57,88 +50,62 @@ function NavigationMenu() {
const handleFullscreen = () => toggleFullScreen();
const handleMirror = () => toggleMirror();
const showEditFormDrawer = () => {
searchParams.set('edit', 'true');
setSearchParams(searchParams);
};
return createPortal(
<div id='navigation-menu-portal' ref={menuRef}>
<RenameClientModal isOpen={isOpen} onClose={onClose} />
<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}
role='button'
onClick={onOpen}
onKeyDown={(event) => {
isKeyEnter(event) && onOpen();
}}
>
Rename Client
</div>
</div>
<hr className={style.separator} />
<Link to='/cuesheet' className={style.link} tabIndex={0}>
Cuesheet
<IoArrowUp className={style.linkIcon} />
</Link>
<Link to='/op' className={style.link} tabIndex={0}>
Operator
<IoArrowUp className={style.linkIcon} />
</Link>
<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>,
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
<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,
);
}
export default memo(NavigationMenu);
@@ -1,5 +0,0 @@
.modalBody {
display: flex;
flex-direction: column;
gap: 1rem;
}
@@ -1,73 +0,0 @@
import { useEffect, useState } from 'react';
import {
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';
import { setClientName } from '../../../hooks/useSocket';
import { useSocketClientName } from '../../../stores/connectionName';
import style from './RenameClientModal.module.scss';
interface RenameClientModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function RenameClientModal({ isOpen, onClose }: RenameClientModalProps) {
const { name: clientName, persistName } = useSocketClientName();
const [newName, setNewName] = useState(clientName);
useEffect(() => {
setNewName(clientName);
}, [isOpen, clientName]);
const handleRename = async () => {
if (newName) {
await setClientName(newName);
persistName(newName);
onClose();
}
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size='sm'
closeOnOverlayClick={false}
motionPreset='slideInBottom'
scrollBehavior='inside'
preserveScrollBarGap
variant='ontime-small'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Rename client</ModalHeader>
<ModalCloseButton />
<ModalBody className={style.modalBody}>
<Input
placeholder='Connection must have a name'
defaultValue={newName}
onChange={(e) => setNewName(e.target.value)}
variant='ontime-filled-on-light'
/>
<Button
isDisabled={newName === clientName || !newName}
onClick={handleRename}
width='100%'
variant='ontime-filled'
>
Save
</Button>
</ModalBody>
</ModalContent>
</Modal>
);
}
@@ -1,42 +0,0 @@
import { Tooltip } from '@chakra-ui/react';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { Playback } from 'ontime-types';
import { tooltipDelayFast } from '../../../ontimeConfig';
interface PlaybackIconProps {
state: Playback;
skipTooltip?: boolean;
className?: string;
}
export default function PlaybackIcon(props: PlaybackIconProps) {
const { state, skipTooltip, className } = props;
// if timer is Pause or Armed
let label = 'Timer Paused';
let Icon = IoPause;
if (state === Playback.Roll) {
label = 'Timer Rolling';
Icon = IoPlay;
} else if (state === Playback.Play) {
label = 'Timer Playing';
Icon = IoPlay;
} else if (state === Playback.Stop) {
label = 'Timer Stopped';
Icon = IoStop;
}
if (skipTooltip) {
return <Icon className={className} />;
}
return (
<Tooltip openDelay={tooltipDelayFast} label={label} shouldWrapChildren>
<Icon className={className} />
</Tooltip>
);
}
@@ -7,7 +7,7 @@ $progress-bar-br: 6px;
width: 100%;
height: $progress-bar-size;
border-radius: $progress-bar-br;
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
background-color: var(--card-background-color-override, $viewer-card-bg-color);
&--hidden {
display: none;
@@ -16,8 +16,8 @@ $progress-bar-br: 6px;
.progress-bar__indicator {
height: $progress-bar-size;
background-color: var(--timer-progress-override, $accent-color);
border-radius: $progress-bar-br;
background-color: var(--accent-color-override, $accent-color);
transition: 1s linear;
border-radius: $progress-bar-br 0 0 $progress-bar-br;
transition-property: width;
}
@@ -16,7 +16,10 @@ export default function ProgressBar(props: ProgressBarProps) {
return (
<div className={`progress-bar__bg ${hidden ? 'progress-bar__bg--hidden' : ''} ${className}`}>
<div className='progress-bar__indicator' style={{ width: `${percentComplete}%` }} />
<div
className='progress-bar__indicator'
style={{ width: `${percentComplete}%` }}
/>
</div>
);
}
@@ -1,64 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { HStack, IconButton, PinInput, PinInputField } from '@chakra-ui/react';
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
import style from './ProtectRoute.module.scss';
interface PinPageProps {
permission: 'editor' | 'operator';
handleValidation: (pin: string) => boolean;
}
export default function PinPage(props: PinPageProps) {
const { permission, handleValidation } = props;
const [pin, setPin] = useState('');
const [failed, setFailed] = useState(false);
const validate = useCallback(() => {
const isValid = handleValidation(pin);
if (!isValid) {
setFailed(true);
setPin('');
}
}, [handleValidation, pin]);
useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.repeat) return;
if (event.key === 'Enter') {
validate();
}
};
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [validate]);
return (
<div className={style.container}>
{`Ontime ${permission || ''}`}
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
<PinInput
type='alphanumeric'
size='lg'
mask
autoFocus
value={pin}
onChange={(value) => {
setFailed(false);
setPin(value);
}}
>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
<IconButton aria-label='Enter' size='lg' isRound icon={<IoCheckmark />} onClick={validate} />
</HStack>
</div>
);
}
@@ -0,0 +1,91 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { HStack, IconButton, PinInput, PinInputField } from '@chakra-ui/react';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import PropTypes from 'prop-types';
import { AppContext } from '../../context/AppContext';
import style from './ProtectRoute.module.scss';
export default function ProtectRoute({ children }) {
const isLocal =
window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const [pin, setPin] = useState('');
const [failed, setFailed] = useState(false);
const { auth, validate } = useContext(AppContext);
const handleValidation = useCallback(() => {
const r = validate(pin);
if (!r) {
setFailed(true);
setPin('');
}
}, [pin, validate]);
// Set window title
useEffect(() => {
document.title = 'ontime';
}, []);
// Handle keyboard shortcuts
const handleKeyPress = useCallback(
(e) => {
// handle held key
if (e.repeat) return;
// Space bar
if (e.keyCode === 13) {
handleValidation();
}
},
[handleValidation]
);
useEffect(() => {
// attach the event listener
document.addEventListener('keydown', handleKeyPress);
// remove the event listener
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [handleKeyPress]);
if (isLocal || auth) {
return children;
}
return (
<div className={style.container}>
ontime
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
<PinInput
type='alphanumeric'
size='lg'
mask
autoFocus
value={pin}
onChange={(value) => {
setFailed(false);
setPin(value);
}}
>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
<IconButton
aria-label='Enter'
size='lg'
isRound
icon={<FiCheck />}
onClick={() => handleValidation()}
/>
</HStack>
</div>
);
}
ProtectRoute.propTypes = {
children: PropTypes.node.isRequired,
};
@@ -1,38 +0,0 @@
import { PropsWithChildren, useCallback, useContext } from 'react';
import { AppContext } from '../../context/AppContext';
import PinPage from './PinPage';
interface ProtectRouteProps {
permission: 'editor' | 'operator';
}
export default function ProtectRoute({ permission, children }: PropsWithChildren<ProtectRouteProps>) {
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const { editorAuth, operatorAuth, validate } = useContext(AppContext);
const handleValidation = useCallback(
(pin: string) => {
return validate(pin, permission);
},
[permission, validate],
);
const hasRelevantAuth = () => {
if (permission === 'editor') {
return editorAuth;
}
if (permission === 'operator') {
return operatorAuth;
}
return false;
};
if (isLocal || hasRelevantAuth()) {
// eslint-disable-next-line react/jsx-no-useless-fragment -- trying to make typescript happy
return <>{children}</>;
}
return <PinPage permission={permission} handleValidation={handleValidation} />;
}
@@ -32,7 +32,7 @@
}
&:not(:last-child) {
padding-bottom: 8px;
padding-bottom: clamp(16px, 1.5vw, 24px);
}
&--past {
@@ -1,19 +1,19 @@
import Empty from '../state/Empty';
import { useSchedule } from './ScheduleContext';
import ScheduleItem from './ScheduleItem';
import './Schedule.scss';
interface ScheduleProps {
isProduction?: boolean;
className?: string;
}
export default function Schedule({ isProduction, className }: ScheduleProps) {
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
export default function Schedule({ className }: ScheduleProps) {
const { paginatedEvents, selectedEventId, isBackstage } = useSchedule();
// TODO: design a nice placeholder for empty schedules
if (paginatedEvents?.length < 1) {
return null;
return <Empty text='No events to show' />;
}
let selectedState: 'past' | 'now' | 'future' = 'past';
@@ -21,25 +21,17 @@ export default function Schedule({ isProduction, 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';
}
const timeStart = isProduction ? event.timeStart + (event?.delay ?? 0) : event.timeStart;
const timeEnd = isProduction ? event.timeEnd + (event?.delay ?? 0) : event.timeEnd;
return (
<ScheduleItem
key={event.id}
selected={selectedState}
timeStart={timeStart}
timeEnd={timeEnd}
timeStart={event.timeStart}
timeEnd={event.timeEnd}
title={event.title}
colour={isBackstage ? event.colour : ''}
backstageEvent={!event.isPublic}
@@ -1,15 +1,12 @@
import { createContext, PropsWithChildren, useContext, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { OntimeEvent } from 'ontime-types';
import { useInterval } from '../../hooks/useInterval';
import { isStringBoolean } from '../../utils/viewUtils';
interface ScheduleContextState {
events: OntimeEvent[];
paginatedEvents: OntimeEvent[];
selectedEventId: string | null;
scheduleType: 'past' | 'now' | 'future';
selectedEventId: string;
numPages: number;
visiblePage: number;
isBackstage: boolean;
@@ -19,56 +16,32 @@ const ScheduleContext = createContext<ScheduleContextState | undefined>(undefine
interface ScheduleProviderProps {
events: OntimeEvent[];
selectedEventId: string | null;
selectedEventId: string;
isBackstage?: boolean;
eventsPerPage?: number;
time?: number;
}
export const ScheduleProvider = ({
children,
events,
selectedEventId,
isBackstage = false,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
export const ScheduleProvider = (
{
children,
events,
selectedEventId,
isBackstage = false,
eventsPerPage = 4,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
const [visiblePage, setVisiblePage] = useState(0);
const [searchParams] = useSearchParams();
// look for overrides from views
const hidePast = isStringBoolean(searchParams.get('hidePast'));
const stopCycle = isStringBoolean(searchParams.get('stopCycle'));
const eventsPerPage = Number(searchParams.get('eventsPerPage') ?? 7);
let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
const viewEvents = [...events];
if (hidePast) {
// we want to show the event after the next
viewEvents.splice(0, selectedEventIndex + 2);
selectedEventIndex = 0;
}
const numPages = Math.ceil(viewEvents.length / eventsPerPage);
const numPages = Math.ceil(events.length / eventsPerPage);
const eventStart = eventsPerPage * visiblePage;
const eventEnd = eventsPerPage * (visiblePage + 1);
const paginatedEvents = viewEvents.slice(eventStart, eventEnd);
const resolveScheduleType = () => {
if (selectedEventIndex >= eventStart && selectedEventIndex < eventEnd) {
return 'now';
}
if (selectedEventIndex > eventEnd) {
return 'past';
}
return 'future';
};
const scheduleType = resolveScheduleType();
const paginatedEvents = events.slice(eventStart, eventEnd);
// every SCROLL_TIME go to the next array
useInterval(() => {
if (stopCycle) {
setVisiblePage(0);
} else if (events.length > eventsPerPage) {
if (events.length > eventsPerPage) {
const next = (visiblePage + 1) % numPages;
setVisiblePage(next);
}
@@ -80,7 +53,6 @@ export const ScheduleProvider = ({
events,
paginatedEvents,
selectedEventId,
scheduleType,
numPages,
visiblePage,
isBackstage,
@@ -1,12 +1,7 @@
import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime';
import { formatTime } from '../../utils/time';
import './Schedule.scss';
const formatOptions = {
format: 'hh:mm a',
};
interface ScheduleItemProps {
selected: 'past' | 'now' | 'future';
timeStart: number;
@@ -19,10 +14,19 @@ interface ScheduleItemProps {
}
export default function ScheduleItem(props: ScheduleItemProps) {
const { selected, timeStart, timeEnd, title, presenter, backstageEvent, colour, skip } = props;
const {
selected,
timeStart,
timeEnd,
title,
presenter,
backstageEvent,
colour,
skip,
} = props;
const start = formatTime(timeStart, formatOptions);
const end = formatTime(timeEnd, formatOptions);
const start = formatTime(timeStart, { format: 'hh:mm' });
const end = formatTime(timeEnd, { format: 'hh:mm' });
const userColour = colour !== '' ? colour : '';
const selectStyle = `entry--${selected}`;
@@ -30,15 +34,12 @@ export default function ScheduleItem(props: ScheduleItemProps) {
<li className={`entry ${selectStyle} ${skip ? 'skip' : ''}`}>
<div className='entry-times'>
<span className='entry-colour' style={{ backgroundColor: userColour }} />
<div style={{ display: 'flex' }}>
<SuperscriptTime time={start} />
{' → '}
<SuperscriptTime time={end} />
{backstageEvent ? '*' : ''}
</div>
{`${start}${end} ${backstageEvent ? '*' : ''}`}
</div>
<div className='entry-title'>{title}</div>
{presenter && <div className='entry-presenter'>{presenter}</div>}
{presenter && (
<div className='entry-presenter'>{presenter}</div>
)}
</li>
);
}
@@ -13,11 +13,12 @@ export default function ScheduleNav({ className }: ScheduleNavProps) {
<div className={`schedule-nav ${className}`}>
{numPages > 1 &&
[...Array(numPages).keys()].map((i) => (
<div
key={i}
className={i === visiblePage ? 'schedule-nav__item schedule-nav__item--selected' : 'schedule-nav__item'}
/>
))}
<div
key={i}
className={i === visiblePage ? 'schedule-nav__item schedule-nav__item--selected' : 'schedule-nav__item'}
/>
),
)}
</div>
);
}
@@ -1,5 +1,4 @@
import { CSSProperties } from 'react';
import { ReactComponent as Emptyimage } from '@/assets/images/empty.svg';
import style from './Empty.module.scss';

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