Compare commits

..

145 Commits

Author SHA1 Message Date
Alex Christoffer Rasmussen f5e9a2549e Improve spreadsheet import flow (#839)
* feat: add dropdown to worksheet selection

* feat: move revoke to separate button

* refactor: extract excel flow to separate controller and service

* chore: show spinner when uploading sheet and disable the other option

* chore: don't show "success" when canceling the import

* chore: export boolean types properly to sheets
2024-03-22 21:33:11 +01:00
Carlos Valente f4692db021 refactor: allow transition from roll to playback (#847) 2024-03-22 13:32:36 +01:00
Carlos Valente dddb11ff40 refactor: granular updates to rundown (#848) 2024-03-22 13:21:49 +01:00
Carlos Valente 6178ed1e4e Apply project (#843) 2024-03-22 12:52:55 +01:00
Carlos Valente 8c5aaa0901 fix: account for midnight rollover (#846) 2024-03-22 08:04:23 +01:00
Carlos Valente b06ab15190 refactor: save object on restore (#842) 2024-03-21 22:10:54 +01:00
Ary a48be0f017 Fix to lastLoadedProject being null (#830) 2024-03-21 21:08:45 +01:00
Alex Christoffer Rasmussen c2280da61f fix: uncaught error in http integration (#837) 2024-03-21 21:07:39 +01:00
Carlos Valente fe30752130 refactor: follow in run mode (#840) 2024-03-21 21:07:15 +01:00
Alex Christoffer Rasmussen a20d63451b fix: use cross-env to allow windows to build local docker (#838) 2024-03-21 00:12:17 +01:00
Carlos Valente 93622e9aeb fix: prevent calculation with no timer (#835) 2024-03-20 12:52:32 +01:00
Carlos Valente 1568af97fc chore: bump node version in docker (#834) 2024-03-19 21:55:37 +01:00
Carlos Valente 4bb836b6f2 End pause (#832)
* refactor: trigger ahead

* refactor: refresh often

* fix: handle empty rundown updates

* fix: handle falsy values

* style: avoid overlap of components

* refactor: freeze end option
2024-03-19 18:04:05 +01:00
Carlos Valente 8156da03d5 Quick options (#814)
* refactor: link start is quick option

* refactor: add default duration

* chore: use last time by default
2024-03-19 17:23:32 +01:00
Carlos Valente dfade25a7c feat: url navigation for settings (#821) 2024-03-18 20:36:02 +01:00
Carlos Valente 4b21745fc7 refactor: download files from interface (#831) 2024-03-18 20:27:58 +01:00
Carlos Valente a91a8a6358 refactor: simplify time-to-end (#828)
* refactor: simplify time-to-end

* refactor: rundown metadata

* refactor: handle overflow in UI

* refactor: show gaps between days

---------

Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>
2024-03-18 17:24:11 +01:00
Carlos Valente 4e6b833e10 refactor: obfuscated pincode in transit 2024-03-18 13:40:59 +01:00
Alex Christoffer Rasmussen 3fdb1bd1c8 partial ws update for demo (#813)
* partial ws update

* extract more of the timer formatting
2024-03-18 12:01:36 +01:00
Carlos Valente 4148a3835b style: clarify state of external message 2024-03-17 19:46:48 +01:00
Carlos Valente 4fb9c42aef refactor: skip expected end if not playing 2024-03-17 19:46:25 +01:00
Carlos Valente dd0dcfc2a2 fix: reset focus when deleting selected 2024-03-17 19:46:10 +01:00
Carlos Valente d41d1b054d fix: reset focus when deleting all 2024-03-17 19:46:10 +01:00
Carlos Valente 8b6e06150a fix: update cache on delete-all (#827) 2024-03-17 19:45:34 +01:00
Carlos Valente a9e0e2b091 refactor: tweaks to chakra theme config (#824) 2024-03-16 09:42:06 +01:00
Carlos Valente 0a8155b6e3 Param nav (#822)
* refactor: remove unused

* refactor: simplify logic
2024-03-15 16:52:33 +01:00
Carlos Valente 118c29e5c2 refactor: filter sentry errors (#816) 2024-03-15 16:13:26 +01:00
Carlos Valente 105f395e6e refactor: migrate fullscreen to mantine (#819) 2024-03-14 18:32:07 +01:00
Carlos Valente 12d9e5922c Alpha revision (#811)
* style: less space for direction button

* chore: cleanup unused

* refactor: prevent CSS pollution

* refactor: force render on swatch

* refactor: force render on swatch

* refactor: improve import of blocks and skip import

* chore: remove unused

* chore: clarify effect

* chore: updata dataset

* fix: integrations not triggering

* chore: remove unused

* fix: onair derives from playback

* fix: gap day after
2024-03-12 21:30:16 +01:00
Carlos Valente 63983fc39a Alpha release (#810)
* fix: path to resources

* docs: rename aliases > url presets
2024-03-09 22:44:35 +01:00
Carlos Valente 41af3b7466 Sheets revision (#809)
* refactor: poll for authentication

* refactor: show success card
2024-03-09 21:39:11 +01:00
Carlos Valente 78b386742e Settings style (#808) 2024-03-09 10:56:14 +01:00
Carlos Valente ad71793cf3 Url presets (#807) 2024-03-08 23:10:43 +01:00
Carlos Valente 10d7273ec0 Skip fixes (#805)
* refactor: resolve current in time-to-end

* refactor: recalculate on skip

* Alpha tweaks (#806)

* style: small tweaks to interface

* refactor: simplify quick add
2024-03-08 19:54:18 +01:00
Carlos Valente 6dc2bebeee Time to end (#804) 2024-03-07 22:10:21 +01:00
Carlos Valente d59885fc20 fix: allow renaming custom field (#803)
* fix: allow renaming custom field

* style: change time-to-end icon
2024-03-07 19:59:36 +01:00
Carlos Valente d22cc48565 refactor: small tweaks and fixes 2024-03-06 22:42:20 +01:00
Carlos Valente 8196ea584d UI tweaks (#800)
* style: placeholder for progress value

* style: rundown menu

* style: extracted rundown takes whole screen

* style: tweaks to rundown indicators

* feat: allow exporting info

* style: indicate action
2024-03-03 20:42:36 +01:00
Carlos Valente 0c31438e44 chore: migrate remaining settings (#799)
* chore: migrate remaining settings

---------

Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com>
2024-03-03 19:16:25 +01:00
Bianca Procopio 10d3986832 Move app settings (#765)
* refactor: migrate app settings

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>
2024-03-03 19:06:21 +01:00
Carlos Valente 088927bbbb refactor: organise API around resources (#798) 2024-03-02 23:08:08 +01:00
Carlos Valente d87ba12b2e chore: cleanup linter (#797) 2024-03-01 23:19:05 +01:00
Carlos Valente 5b01329c37 refactor: migrate project upload (#796)
* refactor: migrate project upload
2024-03-01 22:59:00 +01:00
Carlos Valente 8df419d835 refactor: deprecate presenter and subtitle (#795)
* refactor: deprecate presenter and subtitle

* style: reduce spacing between related sections

* refactor: optional secondary field

* refactor: remove secondary field

* refactor: dynamic data source
2024-02-29 18:19:58 +01:00
Carlos Valente d7392b93d2 refactor: remove userFields (#791)
* refactor: process custom fields on cache generate

* refactor: remove userFields
2024-02-27 13:23:12 +01:00
Carlos Valente 429df21557 Custom fields caching (#790)
* refactor: process custom fields on cache generate
2024-02-25 17:21:59 +01:00
Carlos Valente da7ef59216 Custom fields views (#789)
* style: show field colour in editor

* feat: custom fields in cuesheet

* feat: custom fields in operator

* refactor: update CSV export
2024-02-24 22:04:20 +01:00
Alex Christoffer Rasmussen 474f1e2177 Sheet use limited input device auth flow (#782)
* refactor: limited-input-device auth

* refactor: resolve sheet directory from setup

* refactor: extract sheet logic in backend

* refactor: simplify sheet integration

---------

Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Carlos Valente <carlosvalente@pm.me>
2024-02-24 11:58:50 +01:00
Carlos Valente fc5338903b feat: generate crash report (#787) 2024-02-23 08:52:08 +01:00
cv 11d06de133 Merge branch 'v3' of https://github.com/cpvalente/ontime into v3 2024-02-19 21:43:00 +01:00
cv cbc2ae5116 chore: cleanup merge 2024-02-19 21:42:38 +01:00
cv a6ff874a2a Merge remote-tracking branch 'origin/master' into v3 2024-02-19 21:40:56 +01:00
Carlos Valente 7ff75835ff refactor: partial state updates (#780)
* refactor: partial state updates

* refactor: less calls to restore

* refactor: add error stack trace to log
2024-02-19 21:31:27 +01:00
Carlos Valente c890251dad refactor: delays account for gaps (#784) 2024-02-19 21:30:52 +01:00
Carlos Valente c1e2700c38 style: tweaks to lower thirds (#785) 2024-02-19 21:29:55 +01:00
Carlos Valente 5a397c6da7 Sheets settings (#774)
* wip: migrate sheet settings

---------

Co-authored-by: arc-alex <ac@omnivox.dk>
2024-02-17 22:05:38 +01:00
Alex Christoffer Rasmussen c1377544a0 custom fields (#744)
---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>
2024-02-17 21:55:46 +01:00
Carlos Valente 1a420a1ddd Over under (#771)
* feat: schedule offset
2024-02-16 21:04:58 +01:00
Fabian Posenau 436a34aaee upgraded pipeline actions (#777) 2024-02-15 14:54:05 +01:00
Fabian Posenau 4848fdbee3 Fix build local (#778)
* adjust node version

* fix build:local
2024-02-15 14:51:19 +01:00
Carlos Valente 71dace669b fix: recover cursor logic (#773) 2024-02-13 18:43:41 +01:00
Carlos Valente 67c448572c style: tweaks to settings (#772) 2024-02-12 21:24:49 +01:00
Carlos Valente 53963a9ad7 Integrations data (#770)
* feat: OSC settings

* feat: HTTP settings
2024-02-11 21:25:23 +01:00
Carlos Valente 5355e45b80 refactor: UI for linking events (#763) 2024-02-11 15:18:04 +01:00
Ary 12f81c63ce Project Manager: Quick Start (#762) 2024-02-10 14:30:33 +01:00
Carlos Valente 7f78fac162 wip: UI for integrations (#769) 2024-02-09 13:56:04 +01:00
Carlos Valente e8b5cb6750 wip: add UI to manage custom fields (#767) 2024-02-09 13:51:40 +01:00
Carlos Valente 212e5f9e68 refactor: small type improvements (#759) 2024-02-03 23:17:36 +01:00
Carlos Valente 1890fc49d7 refactor: normalise data (#756)
* chore: remove legal from bundle

* refactor: create normalised dataset

* refactor: cuesheet uses flat rundown

* refactor: multi-selection

* refactor: prevent stale data on server restart

* refactor: increase ID size

* chore: instrument operation

* chore: update csv tests

* fix: resolve directory to test-db (#758)
2024-02-03 21:43:17 +01:00
Ary 47a519bac1 Merge pull request #753 from biancahpp/feat/download-and-export-options
Project Manager: Export
2024-02-01 16:03:41 -07:00
Ary 083b376113 Merge pull request #754 from cpvalente/fix/project_manager_init
Fix: Project manager list is blank on the first app load
2024-02-01 16:03:13 -07:00
Ary 5b20a47ff6 fix: fixed issue where there were no projects on the project list on the first load 2024-01-31 20:28:39 -07:00
Bianca Procopio c3880cd5a7 update downloadCSV and downloadRundown to accept a file name and add two new options to ProjectListItem 2024-01-31 19:11:00 -07:00
Carlos Valente 91fd275564 refactor: rundown service fetches data from data provider (#752) 2024-01-31 19:30:44 +01:00
Carlos Valente cf08bfc796 refactor: type improvements (#751) 2024-01-31 14:42:33 +01:00
Carlos Valente 7eb9ee8d03 refactor: flatten state mutations (#750) 2024-01-31 14:02:27 +01:00
Carlos Valente ec8952be56 chore: organise delay and rundown utils (#749) 2024-01-30 21:26:37 +01:00
Carlos Valente 6a5020424b refactor: improve typing in backend (#748) 2024-01-30 21:12:19 +01:00
Carlos Valente 39711c1edd refactor: time-to-end always updates (#739) 2024-01-30 12:35:54 +01:00
Ary 3c83f4d15b Project manager (#697)
* Project manager: Load, Rename, Duplicate (#712)

* Project Manager: Create, Delete (#723)
2024-01-29 14:06:33 +01:00
Carlos Valente 4c77d26a71 style: tweaks to editor (#738) 2024-01-28 22:42:19 +01:00
Carlos Valente 5de40e8679 fix: update timer on hot reload (#741) 2024-01-27 22:54:03 +01:00
Carlos Valente 32d70b3cd5 chore: split vendor bundle (#740) 2024-01-27 22:30:23 +01:00
Carlos Valente 86cec56a3d refactor: allow import of blocks and skip import (#735) 2024-01-27 15:08:01 +01:00
Carlos Valente f6557a9814 style: editor tweaks (#736)
* style: menu tweaks

* style: editor tweaks

* style: freeze
2024-01-26 14:47:27 +01:00
Carlos Valente c146954a2f refactor: excel cleanup (#734)
* refactor: remove import of project data from excel
2024-01-26 12:55:35 +01:00
Carlos Valente 2df3d376ad feat: many timers (#706)
* wip: create simple timer class

* refactor: service to manage extra timers

* wip:ui for controlling the extra timer

---------

Co-authored-by: arc-alex <ac@omnivox.dk>
2024-01-25 16:51:20 +01:00
Carlos Valente 74e1ae609c refactor: update timers (#729)
* refactor: remove duplication

* refactor: previous times

* refactor: event patching
2024-01-25 13:36:45 +01:00
Carlos Valente e748bff0dd chore: upload screenshots to artifacts (#730) 2024-01-24 21:56:01 +01:00
Carlos Valente b4ad533e6a refactor: cleanup store (#727)
* refactor: isolate clock

* refactor: isolate private

* refactor: remove duplicate

* chore: rename loaded > runtime

* fix: handle no events loaded
2024-01-24 00:20:10 +01:00
Alex Christoffer Rasmussen 0cd2ca4191 update API - part 1 (#709)
* api: `test-ontime` -> `version`

* api: `ontime-poll` -> `poll`

* api: `start-next` -> `startnext`

* api: `start-next` ->  `startnext`

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Carlos Valente <carlosvalente@pm.me>
2024-01-23 08:43:00 +01:00
Carlos Valente cb8ba776e2 Event editor continue (#722)
* refactor: remove info panel

* feat: editor panel

* wip: event info in drawer

* chore: update tests

* chore: add scss to processor
2024-01-22 16:23:11 +01:00
Carlos Valente c7328a33f6 chore: fail tests that contain unhandled errors (#724) 2024-01-22 14:15:23 +01:00
Carlos Valente fc5800bc21 remove info panel (#721)
* refactor: remove info panel

* chore: update tests
2024-01-19 14:08:32 +01:00
Carlos Valente 3c1be1fc10 fix: issue with loosing cursor position on message (#719) 2024-01-17 23:16:22 +01:00
Carlos Valente a2950442d8 refactor: runtime service (#715)
* refactor: rename PlaybackService > RuntimeService

* refactor: stateful runtime service

* refactor: merge event loading

* refactor: roll is part of state mutation
2024-01-16 19:54:40 +01:00
Alex Christoffer Rasmussen ee121bcfcb change all ExcelImportMaps to lowercase (#716)
* fix: change all ExcelImportMaps to lowercase (#716)
2024-01-16 19:35:16 +01:00
Alex Christoffer Rasmussen 4234334fbb fix: test - go to Edit mode befor tying to click Event options button (#708) 2024-01-13 12:45:23 +01:00
Carlos Valente 7f63cde7a5 feat: multiple selection (#703)
feat: multiple event selection

---------

Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com>
Co-authored-by: Alex <ac@omnivox.dk>
2024-01-12 13:07:56 +01:00
Alex Rohleder f965db148c refactor: setup global timer state (#629)
* refactor: setup global timer state

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Co-authored-by: Carlos Valente <carlosvalente@pm.me>
2024-01-12 13:03:58 +01:00
cv e6bb52ad9c chore: cleanup merge 2024-01-12 10:17:12 +01:00
cv 6d067f3d3a chore: cleanup merge 2024-01-12 10:13:49 +01:00
cv 506f9a868b chore: cleanup merge 2024-01-12 10:13:40 +01:00
cv adb8981191 chore: cleanup merge 2024-01-12 10:12:01 +01:00
cv a0b3e56896 chore: cleanup merge 2024-01-12 10:05:51 +01:00
cv 229fff05a4 Merge remote-tracking branch 'origin/master' into v3 2024-01-12 10:05:10 +01:00
Alex Christoffer Rasmussen 5b7045e6a5 chore: remove unused deepmerge (#705) 2024-01-10 19:30:34 +01:00
Alex Christoffer Rasmussen 7cc4364a63 chore: upgrade nanoid to v5 (#704) 2024-01-10 19:28:34 +01:00
Carlos Valente b4e73ff6b5 Refactor/time formatting (#696)
* chore: upgrade related dependencies

* fix: skip sentry on dev

* refactor: unify time formatting

* refactor: display default format for page
2024-01-09 15:30:11 +01:00
Carlos Valente 9031051465 Revert "style: align delay to clock"
This reverts commit 3fb862628c.
2024-01-08 22:55:57 +01:00
Carlos Valente a15a9671d2 Revert "chore: cleanup utils"
This reverts commit ecd13fdc5a.
2024-01-08 22:55:47 +01:00
Carlos Valente ecd13fdc5a chore: cleanup utils 2024-01-08 22:51:38 +01:00
Carlos Valente 3fb862628c style: align delay to clock 2024-01-08 22:48:25 +01:00
Carlos Valente 1f3133d4f3 chore: upgrade deps (#698) 2024-01-08 17:31:04 +01:00
Alex Christoffer Rasmussen 64573aff8a refactor: update lower third (#665)
* init

* auto in out

* fix in out animation and other options

* move some constants to css override

* dont show block if text source is empty

* select trigger

* adjust the animation

* remove the old one and replace it in the router

* return null from useEffect when there is no timer to clear

* typo: manuel -> manual

* fix: delay is excluding transition in time

* rename to top and bottom

* change font size to px and increese the default

* sharper animation timning

* fix: dont need state for options

* fix: e2e test for lower third

* deafault to ffffff00 key color

* typo: scss naming

* style: all white default bg key

* style: add default values to src options

* style: `white-space: nowrap;`

* style: start out animation a bit early

* style: browser tab title

* fix: checking the correct default values in ViewParamsEditor

* fix: defaultValues save improved by @asharonbaltazar

* lint: capitalize enum names

* fix: memoize options

* fix: searchParams test

* fix: readability of `topText` and `bottomText` and memoize them

* fix: enum naming upper case

* refactor: remove cx import

* refactor: try to simplify is else flow
2024-01-07 23:23:25 +01:00
Carlos Valente 655e9e8fb5 refactor: migrate logs (#673) 2024-01-06 21:48:20 +01:00
Carlos Valente bac2dc6b87 feat: create project list (#671) 2024-01-06 21:38:16 +01:00
Ary 65a5aaa665 Keep several project files in user folder - Steps 5A-E (#684) 2024-01-06 21:24:48 +01:00
Carlos Valente c889d00e6b fix: focus cursor (#695)
* fix: prevent cursor from removing focus
2024-01-06 09:10:42 +01:00
Carlos Valente 3753d51d85 chore: cleanup 2024-01-05 21:57:47 +01:00
Carlos Valente cb23b83b9d Merge remote-tracking branch 'origin/master' into v3 2024-01-05 21:55:16 +01:00
Alex Christoffer Rasmussen a567fdc446 build: upgrade lowdb (#689) 2024-01-02 20:27:10 +01:00
Carlos Valente 3d9a9a2226 wip: overview (#688)
* wip: overview
2024-01-01 22:06:23 +01:00
Carlos Valente 9a4d659b04 improvement: hide seconds (#675)
* feat: optional timer seconds
2023-12-31 12:13:05 +01:00
Fabian Posenau a5d8b91f34 add fields to import modal (#685)
Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-12-30 22:11:06 +01:00
Carlos Valente 5409c0ac6e refactor: stabilise actionHandler (#683) 2023-12-30 21:13:51 +01:00
Fabian Posenau 3998b8927d Warning and danger per event (#677)
* feat: warning and danger per event

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
2023-12-30 20:56:57 +01:00
Ary 7b9f64a630 Project files tweaks (#679)
* chore: fixed issue when loading first time; deleting excel file after upload; only json files from project list

* chore: added last loaded project to endpoint response

* chore: incremental file name

---------

Co-authored-by: Name <Email>
2023-12-30 19:32:17 +01:00
Alex Christoffer Rasmussen c5c0401ac4 refactor: switch out steno for lowdb (#682)
* refactor: switch out steno for lowdb
2023-12-30 10:03:02 +01:00
Alex Christoffer Rasmussen 96280e5932 Feat: reorder events with alt+ctrl + arrow up/down (#645)
* fix: key guard

* feat: reorder events

* refactor: return index from the getEvent functions
2023-12-28 13:41:43 +01:00
Alex Christoffer Rasmussen b2c01a141a Studio clock (#663)
* refactor: trim rundown shows only upcoming

* style: small tweaks

---------

Co-authored-by: Carlos Valente <carlosvalente@pm.me>
2023-12-28 13:25:45 +01:00
Ary dc4ba4012c feat: Several project files user folder (Steps 3-5) (#664)
* feat: steps 3-5 for several project files user folder

* feat: common types, better listing function

* feat: error response type
2023-12-28 13:23:51 +01:00
Carlos Valente d1e201ebb5 refactor: remove deprecated event data (#674) 2023-12-27 20:00:09 +01:00
Carlos Valente a463cb491b feat: app settings (#658)
* feat: app settings

* refactor: cleanup routes

* style: smaller base font

* chore: migrate about modal

* fix: import links
2023-12-25 21:25:09 +01:00
Carlos Valente 6ffc314513 chore: automated screenshots (#667)
* chore: take screenshots on routes

* chore: update test data

* chore: update test

* chore: upgrade node in CI
2023-12-25 19:41:45 +01:00
Ary f2632412aa Several project files user folder (#617)
* feat: save loaded file across sessions
2023-12-21 13:52:09 +01:00
Carlos Valente 062dc0c914 refactor: pass HTTP to integration controller (#652)
* refactor: pass HTTP to integration controller

* refactor: deprecate onair control

* refactor: remove playback router
2023-12-19 21:44:43 +01:00
Carlos Valente fe9f12a14b chore: lock node version to electron 2023-12-19 10:23:27 +01:00
Carlos Valente 3144888eb1 chore: add italian to languages (#656) 2023-12-19 10:05:09 +01:00
Carlos Valente 820eacde91 refactor: prevent accidental edit in operator view (#659)
* refactor: prevent accidental edit in operator view
2023-12-19 10:05:09 +01:00
Matteo Gheza 6886a3236f Add italian client translation (#655)
* Add Italian client translation
2023-12-19 10:05:09 +01:00
Carlos Valente bb541c551f chore: upgrade build deps (#661)
* chore: upgrade build dependencies

* chore: add bundle analysis script

* chore: upgrade sentry deps

* chore: upgrade node types

* docs: declare new engine versions
2023-12-19 10:03:59 +01:00
Carlos Valente 3196bd7802 chore: version bump 2023-12-17 20:57:49 +01:00
Carlos Valente ce0657cc6d style: smaller base font 2023-12-17 20:56:58 +01:00
Carlos Valente 44c2b1b5e3 refactor: cleanup routes 2023-12-17 20:56:42 +01:00
1272 changed files with 40279 additions and 77623 deletions
+2 -11
View File
@@ -1,5 +1,5 @@
# Ignore formatter config files
.oxfmt*
# Ignore config files
.prettier*
# Ignore install stuff
**/yarn.lock
@@ -11,13 +11,4 @@
# Ignore git and cache folders
.git
.gitignore
.cache
# Ignore build folders
node_modules
**/node_modules
# Ignore default volumes created by running docker compose up
ontime-db
ontime-styles
+1
View File
@@ -0,0 +1 @@
"ONTIME_VERSION.js"
+52
View File
@@ -0,0 +1,52 @@
{
"root": true,
"parserOptions": {
"ecmaVersion": 2020
},
"env": {
"es6": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "eslint-config-prettier"],
"plugins": ["@typescript-eslint", "prettier"],
"overrides": [
{
"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"]
}
],
"no-restricted-imports": [
"error",
{
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
}
],
"@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"
}
]
}
}
-2
View File
@@ -1,2 +0,0 @@
github: cpvalente
buy_me_a_coffee: cpvalente
Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

-61
View File
@@ -1,61 +0,0 @@
name: Ontime CLI build
on:
release:
types: [published]
workflow_dispatch:
jobs:
build_cli:
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project packages
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm build
- name: Copy server
run: mkdir -p apps/cli/server && cp apps/server/dist/index.cjs apps/cli/server/index.cjs
- name: Copy client
run: cp -R apps/client/build apps/cli/client
- name: Copy external
run: cp -R apps/server/src/external apps/cli/external
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
- name: Install newer version of npm
run: npm install -g npm@11
- name: Publish to NPM
run: pnpm publish --access public --no-git-checks
env:
NPM_CONFIG_PROVENANCE: true
working-directory: ./apps/cli
-67
View File
@@ -1,67 +0,0 @@
name: Docker Image CI Ontime
on:
release:
types: [published]
workflow_dispatch:
jobs:
publish_docker:
runs-on: ubuntu-latest
env:
CI: ''
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project packages
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm build:docker
- name: Docker Login
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Setup Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/ontime
tags: |
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}
type=raw,value=nightly,enable=${{ github.event.release.prerelease == true }}
- name: Build and push stable release
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
# Push is a shorthand for --output=type=registry
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+68
View File
@@ -0,0 +1,68 @@
name: Docker Image CI Ontime V2
on:
push:
tags: [ "*" ]
workflow_dispatch:
jobs:
publish_docker:
runs-on: ubuntu-latest
env:
CI: ''
steps:
- 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
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project packages
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm turbo build:docker
- name: Docker Login
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Build and push stable release
if: github.event.release.prerelease == false
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: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
-48
View File
@@ -1,48 +0,0 @@
name: Ontime Resolver build
on:
workflow_dispatch:
jobs:
build_resolver:
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# This step is only needed to setup the permissions to update npm as pnpm will setup the correct node version
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project packages
run: pnpm build:resolver
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
- name: Install newer version of npm
run: npm install -g npm@11
- name: Publish to NPM
run: pnpm publish --access public --no-git-checks
env:
NPM_CONFIG_PROVENANCE: true
working-directory: ./apps/resolver
+108
View File
@@ -0,0 +1,108 @@
name: Ontime build v2
on:
push:
tags: [ "*" ]
workflow_dispatch:
jobs:
build_macos:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project packages
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm build
- name: Electron - Build app
run: pnpm dist-mac
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
./apps/electron/dist/ontime-macOS-x64.dmg
./apps/electron/dist/ontime-macOS-arm64.dmg
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project packages
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm build
- name: Electron - Build app
run: pnpm dist-win
- name: Release
uses: softprops/action-gh-release@v1
with:
files: './apps/electron/dist/ontime-win64.exe'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_ubuntu:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project packages
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm build
- name: Electron - Build app
run: pnpm dist-linux
- name: Release
uses: softprops/action-gh-release@v1
with:
files: './apps/electron/dist/ontime-linux.AppImage'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,8 +1,9 @@
name: Ontime build
name: Ontime build v3
on:
push:
tags: ['*']
# Only trigger manually for now for testing
# push:
# tags: [ "*" ]
workflow_dispatch:
jobs:
@@ -11,20 +12,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
uses: pnpm/action-setup@v3
with:
path: ${{ env.PNPM_STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -35,14 +31,7 @@ jobs:
run: pnpm build
- name: Electron - Build app
env:
APPLE_ID: ${{ secrets.APPLEID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
run: pnpm dist-mac --env-mode=loose
timeout-minutes: 60
run: pnpm dist-mac
- name: Release
uses: softprops/action-gh-release@v1
@@ -58,9 +47,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -85,9 +80,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -103,9 +104,6 @@ jobs:
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
./apps/electron/dist/ontime-linux-x86_64.AppImage
./apps/electron/dist/ontime-linux-arm64.AppImage
./apps/electron/dist/ontime-linux-armv7l.AppImage
files: './apps/electron/dist/ontime-linux.AppImage'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-101
View File
@@ -1,101 +0,0 @@
name: Ontime test
on:
pull_request:
branches: '*'
workflow_dispatch:
jobs:
unit-test:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Run code quality
- name: Run linter
run: pnpm lint
- name: Run TypeScript checks
run: pnpm typecheck
# We choose to run tests separately
- name: Run unit tests
if: always()
run: pnpm test
e2e-test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Get installed Playwright version
run: echo "PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --parseable | cut -s -d '@' -f3 | cut -d '/' -f1)" >> $GITHUB_ENV
- name: Cache playwright binaries
uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
restore-keys: |
${{ runner.os }}-playwright-
- run: npx playwright install --with-deps
if: steps.playwright-cache.outputs.cache-hit != 'true'
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: pnpm e2e
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v4
with:
name: automated-screenshots
path: automated-screenshots/
retention-days: 14
+106
View File
@@ -0,0 +1,106 @@
name: Ontime test v2
on:
pull_request:
branches: '*'
workflow_dispatch:
jobs:
unit-test:
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: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- 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
e2e-test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build client
run: pnpm build:local
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: pnpm e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v3
with:
name: automated-screenshots
path: automated-screenshots/
retention-days: 14
+107
View File
@@ -0,0 +1,107 @@
name: Ontime test v3
on:
# Only trigger manually for now for testing
# pull_request:
# branches: '*'
workflow_dispatch:
jobs:
unit-test:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- 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
e2e-test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build client
run: pnpm build:local
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: pnpm e2e
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v4
with:
name: automated-screenshots
path: automated-screenshots/
retention-days: 14
+4 -9
View File
@@ -28,28 +28,23 @@ test-results
playwright-report
/playwright/.cache/
automated-screenshots
e2e/tests/fixtures/tmp/*
# production
build/
dist/
*.tsbuildinfo
# bundled assets
translations.json
override.css
# working stuff
**/TODO.md
**.local.**
# docker utils
ontime-db
ontime-external/
ontime-data/
# working database
apps/server/src/preloaded-db/db.json
# versioning file
**/ONTIME_VERSION.js
# temporary write files
**.tmp
**.tmp
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm lint-staged
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+51
View File
@@ -0,0 +1,51 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myCustomPropertiesEnabled" value="true" />
<option name="myIgnoreVendorSpecificProperties" value="false" />
<option name="myCustomPropertiesList">
<value>
<list size="3">
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
<item index="2" class="java.lang.String" itemvalue="app-region" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredUrls">
<list>
<option value="http://localhost" />
<option value="http://127.0.0.1" />
<option value="http://0.0.0.0" />
<option value="http://www.w3.org/" />
<option value="http://json-schema.org/draft" />
<option value="http://java.sun.com/" />
<option value="http://xmlns.jcp.org/" />
<option value="http://javafx.com/javafx/" />
<option value="http://javafx.com/fxml" />
<option value="http://maven.apache.org/xsd/" />
<option value="http://maven.apache.org/POM/" />
<option value="http://www.springframework.org/schema/" />
<option value="http://www.springframework.org/tags" />
<option value="http://www.springframework.org/security/tags" />
<option value="http://www.thymeleaf.org" />
<option value="http://www.jboss.org/j2ee/schema/" />
<option value="http://www.jboss.com/xml/ns/" />
<option value="http://www.ibm.com/webservices/xsd" />
<option value="http://activemq.apache.org/schema/" />
<option value="http://schema.cloudfoundry.org/spring/" />
<option value="http://schemas.xmlsoap.org/" />
<option value="http://cxf.apache.org/schemas/" />
<option value="http://primefaces.org/ui" />
<option value="http://tiles.apache.org/" />
<option value="http://__IP__" />
</list>
</option>
</inspection_tool>
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>
+1 -1
View File
@@ -1 +1 @@
v22.15.1
v18.18.2
-25
View File
@@ -1,25 +0,0 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 120,
"sortPackageJson": false,
"sortImports": {},
"ignorePatterns": [
"build",
"coverage",
"dist",
"node_modules",
"playwright-report",
"pnpm-lock.yaml",
"**/*.toml",
"**/*.json",
"!tsconfig.common.json",
"!turbo.json",
"!package.json"
]
}
-37
View File
@@ -1,37 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"categories": {
"correctness": "warn",
"suspicious": "off",
"pedantic": "off",
"perf": "warn",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"rules": {
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"caughtErrors": "none"
}
],
"no-restricted-imports": [
"error",
{
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
}
]
},
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**",
"apps/client/build/**",
"apps/electron/assets/**",
"**/ONTIME_VERSION.js"
]
}
+10
View File
@@ -0,0 +1,10 @@
build
coverage
dist
node_modules
playwright-report
**/*.toml
**/*.yml
**/*.json
+9
View File
@@ -0,0 +1,9 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 120
}
+29 -37
View File
@@ -2,16 +2,16 @@
Ontime consists of 3 distinct parts
- **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
- __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.
Other dependencies will be installed as part of the setup
- **node** (~22)
- **pnpm** (~10)
- **docker** (only necessary to run and build docker images)
- __node__ (~18.18.2)
- __pnpm__ (>=8)
- __docker__ (only necessary to run and build docker images)
## LOCAL DEVELOPMENT
@@ -21,21 +21,18 @@ Locally, we would need to run both the React client and the node.js server in de
From the project root, run the following commands
- **Install the project dependencies** by running `pnpm i`
- **Create a local build** by running `pnpm build`, this will populate local dependencies
- **Run dev mode** by running `pnpm dev` or `pnpm dev:electron` to get the electron window
- __Install the project dependencies__ by running `pnpm i`
- __Run dev mode__ by running `turbo dev`
### Debugging backend
The previous command will start the development servers for both the client, server and electron applications.
Typically in dev mode we prefer to start these in separate terminals to help with error tracking and debugging.
To debug backend code in Node.js:
We do that by creating two terminals an running
- **Run the React UI** by running `pnpm dev --filter=ontime-ui`
- **Run the nodejs server** by running `pnpm dev --filter=ontime-server`
- If you need to set breakpoints and inspect the code execution, enable Node.js inspect mode by running `pnpm dev:inspect --filter=ontime-server`.
- 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
@@ -48,7 +45,7 @@ Generally we have 2 types of tests.
Unit tests are contained in mostly all the apps and packages (client, server and utils)
You can run unit tests by running `pnpm test:pipeline` from the project root.
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
@@ -68,7 +65,7 @@ start the webserver with `pnpm dev:server`
Some other useful commands
- `pnpm e2e:ui` open playwright UI
- `pnpm e2e --ui` open playwright UI
- `pnpm e2e --headed` run tests with a visible browser window
## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux)
@@ -78,9 +75,9 @@ 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 `pnpm build`
- **Create the package** by running `pnpm dist-win`, `pnpm dist-mac` or `pnpm dist-linux`
- __Install the project dependencies__ by running `pnpm i`
- __Build the UI and server__ by running `turbo build:electron`
- __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`
@@ -91,24 +88,19 @@ While it should allow for a generic setup, it might need to be modified to fit y
From the project root, run the following commands
- **Build docker image from** by running `docker build -t getontime/ontime .`
- **Run docker image from compose** by running `docker-compose up -d`
- __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>`
- __List running processes__ by running `docker ps`
- __Kill running process__ by running `docker kill <process-id>`
## CONTRIBUTION GUIDELINES
## General Info
If you want to propose changes to the codebase, please reach out before opening a Pull Request.
# APP Building
For new PRs, please follow the following checklist:
- [ ] You have updated and ran unit locally and they are passing. Unit tests are generally created for all utility functions and business logic
- [ ] You have ran code formatting and linting in all your changes
- [ ] The branch is clean and the commits are meaningfully separated and contain descriptive messages
- [ ] The PR body contains description and motivation for the changes
After this checklist is complete, you can request a review from one of the maintainers to get feedback and approval on the changes. \
We will review as soon as possible
We build the app from app.js for almost all applications. The output file will still be named index.cjs. This is because of Electron.
Building the app from index.ts only applies for applications that don't use electron. index.ts will take over the initialization of the server and UI when electron isn't present.
+27 -30
View File
@@ -1,30 +1,27 @@
ARG NODE_VERSION=22.15.1
FROM node:${NODE_VERSION}-alpine
# Set environment variables
# Environment Variable to signal that we are running production
ENV NODE_ENV=docker
# Ontime Data path
ENV ONTIME_DATA=/data/
RUN mkdir /app
WORKDIR /app/
# Prepare UI
COPY apps/client/build/ ./client/
# Prepare Backend
COPY apps/server/dist/ ./server/
COPY apps/server/src/external/ ./external/
COPY apps/server/src/user/ ./user/
COPY apps/server/src/html/ ./html/
# Export default ports
EXPOSE 4001/tcp 8888/udp 9999/udp
CMD ["node", "server/docker.cjs"]
# Build and run commands
# pnpm build:docker
# docker buildx build . -t getontime/ontime
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/data/ getontime/ontime
FROM node:18.18-alpine
# Set environment variables
# Environment Variable to signal that we are running production
ENV NODE_ENV=docker
# Ontime Data path
ENV ONTIME_DATA=/external/
WORKDIR /app/
# Prepare UI
COPY /apps/client/build ./client/
# Prepare Backend
COPY /apps/server/dist/ ./server/
COPY /demo-db/ ./preloaded-db/
COPY /apps/server/src/external/ ./external/
# Export default ports
EXPOSE 4001/tcp 8888/udp 9999/udp
CMD ["node", "server/docker.cjs"]
# Build and run commands
# !!! Note that this command needs pre-build versions of the UI and server apps
# docker buildx build . -t getontime/ontime
# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/external/db/ -v ./ontime-styles:/external/styles/ getontime/ontime
+170 -102
View File
@@ -1,137 +1,205 @@
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-green.svg)](https://www.gnu.org/licenses/gpl-3.0)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/cpvalente/ontime/total)
![Docker Pulls](https://img.shields.io/docker/pulls/getontime/ontime)
![NPM Downloads](https://img.shields.io/npm/dy/%40getontime%2Fcli)
![Homebrew Cask Version](https://img.shields.io/homebrew/cask/v/ontime)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/cpvalente)
[![](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=%E2%9D%A4&logo=buymeacoffee&color=%23fe8e86)](https://www.buymeacoffee.com/cpvalente)
[![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)
## Download the latest release
## Download the latest releases here
<a href="https://www.buymeacoffee.com/cpvalente" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="32"></a>
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe">Windows</a>
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg">macOS (Apple Silicon)</a>
- Download for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-x64.dmg">macOS (Intel)</a>
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-x86_64.AppImage">Linux (Intel / AMD 64-bit)</a>
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-arm64.AppImage">Linux (ARM 64-bit, Raspberry Pi 4+)</a>
- Download AppImage for <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux-armv7l.AppImage">Linux (ARM 32-bit, older Raspberry Pi)</a>
... or
- Get from <a href="https://hub.docker.com/r/getontime/ontime">Docker hub</a>
- Install from <a href="https://www.npmjs.com/package/@getontime/cli">NPM</a>
- Install from <a href="https://formulae.brew.sh/cask/ontime">Homebrew</a>
## Need help?
We do our best to have most topics covered by the documentation. However, if your question is not covered, you are welcome to [fill in a bug report in an issue](https://github.com/cpvalente/ontime/issues), [ask a question in GitHub discussions](https://github.com/cpvalente/ontime/discussions) or hop in the [discord server](https://discord.com/invite/eje3CSUEXm) for a chat.
## Using Ontime?
Let us know!
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
<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>
</div>
# Ontime
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
Ontime is an application for creating and managing event running order and timers.
With Ontime, you can plan, track your schedule, manage automation and cross-department show information all in one place.
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.
Ontime is made by entertainment and broadcast engineers and used by
This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video outputs.
- Conference organisers
- Touring shows and receiving venues
- Broadcasters and streamers
- Theatres and opera houses
- Houses of worship
![App Window](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/app.png)
## Main features
- **Multiplatform**: Available as a Cloud service and for Windows, macOS, Linux, or self-hosted via Docker.
- **In any device**: Ontime is available to any device with a browser, eg: tablets, mobile phones, laptops, signage, media servers...
- **Team Collaboration**: Dedicated views for directors, operators, backstage, and signage.
- **Real-Time Updates**: Manage and communicate runtime delays effortlessly.
- **Automatable**: Ontime can be fully or partially controlled by an operator, or run standalone with the system clock
- **Flexible Integrations**: Use one of the APIs provided (OSC, HTTP, Websocket) or the available [Companion module](https://bitfocus.io/connections/getontime-ontime) to integrate into your workflow (vMix, disguise, Qlab, OBS)
... and a lot more ...
### For live environments
Ontime is designed for use in live environments. \
This guides the application into being flexible and efficiently integrating into different workflows.
### For teams
All information added in Ontime is shared with the production team and other software / hardware in your workflow. \
Ontime also improves team collaboration with dedicated views for cuesheets and operators, and for public and production
signage.
### Simple infrastructure
All the data is distributed over the network, making its distribution and infrastructure flexible and cheap. \
With the availability of the docker image, you can also leverage IT infrastructure to make Ontime available online for
your team and clients.
Ontime is made by video engineers and entertainment technicians.
![App Window](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/editor.png)
![Views](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/ontime-overview.webp)
![Views](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/overview.png)
[Read the docs to learn more](https://docs.getontime.no)
## Using Ontime
### Getting started
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.
The easiest way to start with Ontime is by leveraging our [Cloud service](https://getontime.no). \
This will give you immediate access to running instances of Ontime which are available to share with anyone with an internet connection.
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.
Alternatively, you can run Ontime locally for free by downloading the latest release for your platform or using the docker image, available at [Docker Hub](https://hub.docker.com/r/getontime/ontime)
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 presets feature
Once installed and running, any device that shares the same network as Ontime will have access to Ontime.
```
For the presentation views
-------------------------------------------------------------
IP.ADDRESS:4001 > Web server default to presenter timer view
IP.ADDRESS:4001/timer > Presenter / Stage timer view
IP.ADDRESS:4001/minimal > Simple timer view
IP.ADDRESS:4001/clock > Simple clock view
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock
```
More information is available [in our docs](https://docs.getontime.no)
```
For management views
-------------------------------------------------------------
IP.ADDRESS:4001/editor > the control interface, same as the app
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
```
## Continued development
More documentation is available [in our docs](https://docs.getontime.no)
Ontime is under active development. We continue adding and improving features in collaboration with users.
## 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)
- [x] Configurable Lower Thirds
- [x] Collaborative editing with the cuesheet view
- [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
- OSC (Open Sound Control)
- HTTP
- WebSockets
- [x] Roll mode: run standalone 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://docs.getontime.no/features/count-to-anything/): 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 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.
## Rich APIs for workflow integrations
The app is currently being developed for a broad user base, from broadcast to entertainment and
conference halls.
Taking advantage of the integrations, we currently use Ontime with:
- `disguise`: trigger Ontime from d3's timeline using the **OSC API**, and **render views** using d3's
webmodule
- `OBS`: **render views** using the Browser Module
- `QLab`: trigger Ontime using **OSC API**
- `Companion`: Ontime has a **companion module**. Issue report and feature requests should be done
in the [repository getontime/ontime](https://github.com/bitfocus/companion-module-getontime-ontime)
### Make your own viewer
Ontime broadcasts its data over WebSockets. This allows you to consume its data outside 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).
<br />
We have prepared a few resources to help here:
- Shipped with Ontime there is a small clock to get you started, it is available at `http://localhost:4001/external/demo` and the [code can be found here](https://github.com/cpvalente/ontime/tree/master/apps/server/src/external/demo)
- See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a template on
how to get you started
- See information about the [Websocket API](https://docs.getontime.no/api/osc-and-ws/)
<br />
More information [in the docs](https://docs.getontime.no/features/custom-views/)
### Headless run
You can self-host and run Ontime in a docker image.
The docker image along with documentation is [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
If you want to run this image in a Raspberry Pi, please see [the docs](https://docs.getontime.no/additional-notes/use-with-rpi/)
## Roadmap
### Continued development
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)
## Issues
### Issues
We use Github's issue tracking for bug reporting and feature requests. \
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).
## Contributing
#### 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.`
or
`Ontime can't be opened because it is from an unidentified developer`
or in Linux
`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 Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
File as a Program
Long story short: Ontime app is unsigned. </br>Purchasing the certificates for both Mac and Windows
would mean a recurrent expense and is not a priority. This is unlikely to change in future. If you
have tips on how to improve this or would like to sponsor the code signing,
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
There is no plan for any further work on this.
# Contributing
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
If you are a developer and would like to contribute with code, please open an issue to discuss before opening a Pull Request.
<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.
<br />
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
## Links
# Help
- [Ontime website](https://getontime.no)
- [Documentation](https://docs.getontime.no)
- [Video tutorials](https://www.youtube.com/@ontimeapp)
- [Ontime discord server](https://discord.com/invite/eje3CSUEXm)
Help is underway! ... and can be found [here](https://docs.getontime.no)
## License
# License
This project is licensed under the terms of the GNU GPL v3
## Sponsor
You can help the development of this project or say thank you with a one time donation. \
See [the terms of donations](https://github.com/cpvalente/ontime/blob/master/.github/FUNDING.md).
<p align="center">
<br>
<a href="https://www.buymeacoffee.com/cpvalente" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="200"></a>
</p>
-24
View File
@@ -1,24 +0,0 @@
# Terms of sponsorship
Ontime does not exist as a fiscal entity. The sponsorship recipient is the project owner. In this light, contributors are funding the project owner's capacity to work on the project, not the project itself. As a contribution recipient, the code owner reserves the right to redistribute.
## Why sponsor
Ontime is and will remain free, regardless of your decision to contribute.
Regardless, you may choose to provide a monetary contribution:
### Improve user experience
Certain parts of the application user experience are tied to our capacity to pay for services. App signing and Google Sheets integration are good examples of this.
### Cool stuff
We may choose not to implement some features. This could be related to the complexity required, to the skill set, or to the fact that we just feel it doesn't improve the app for a significant percentage of users. In those cases, a feature may be considered "not worth it," regardless of its pertinence.
As a user, you can choose to sponsor a particular feature or donate to the app so that we can create bounties for features.
### Priorities
Monetary contributions are most immediately needed to offset the costs of presenting and distributing Ontime, i.e., hosting, domains, and certificates.
Additional funding will be considered according to the needs at the time.
-3
View File
@@ -1,3 +0,0 @@
server
external
client
-5
View File
@@ -1,5 +0,0 @@
{
"$schema": "../../node_modules/oxlint/configuration_schema.json",
"extends": ["../../.oxlintrc.json"],
"plugins": ["unicorn", "typescript", "oxc", "vitest"]
}
-52
View File
@@ -1,52 +0,0 @@
# Ontime CLI
![Ontime](https://github.com/cpvalente/ontime/blob/9e63261b350e5b7b61c7ffb17c9e75a6c5a8c711/.github/aux-images/editor.png)
> Ontime is an application for managing rundown and event timers.
Congratulations! You got this far into Ontime's rabbit hole and want to manage your installation.
The CLI tool is a fast and lightweight way of installing Ontime and is perfect for self-hosting and headless installs.
## Getting started
### Prerequisites
You will need to have installed the correct version of node.js
Please check [the app engines declaration](https://github.com/cpvalente/ontime/blob/master/package.json) for the correct version.
### Running Ontime
To get Ontime running, all you need to do is install it in your system using your package manager of choice:
Install globally in your system
```bash
npm install -g @getontime/cli
```
... and run using the installed script
```bash
ontime
```
Or install and run ontime (the installation here is temporary for the duration of the session)
```bash
npx install @getontime/cli
```
## Links
- [Ontime's repository](https://github.com/cpvalente/ontime)
- [Ontime's documentation](https://docs.getontime.no/)
- [Ontime's website](https://getontime.no/)
## Sponsoring
You can help the development of this project or say thank you with a one time donation. \
See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/cpvalente)
[![](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=%E2%9D%A4&logo=buymeacoffee&color=%23fe8e86)](https://www.buymeacoffee.com/cpvalente)
-20
View File
@@ -1,20 +0,0 @@
#!/usr/bin/env node
// NOTE: for now the following needs to be in place: ./server/index.cjs, ./client, ./external
const ontimeServer = require('./server/index.cjs');
const { initAssets, startServer, startIntegrations, shutdown } = ontimeServer;
async function startOntime() {
await initAssets();
await startServer();
await startIntegrations();
}
startOntime();
process.on(['SIGHUP', 'SIGINT', 'SIGTERM'], () => {
shutdown();
});
-22
View File
@@ -1,22 +0,0 @@
{
"name": "@getontime/cli",
"version": "4.5.0",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
"keywords": [
"lighdev",
"ontime",
"timer"
],
"license": "AGPL-3.0-only",
"main": "main.js",
"bin": {
"ontime": "main.js"
},
"files": [
"client",
"external",
"server"
]
}
+2
View File
@@ -0,0 +1,2 @@
node_modules
build
+141
View File
@@ -0,0 +1,141 @@
{
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true,
"node": true
},
"extends": [
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended"
],
"plugins": [
"react",
"testing-library",
"simple-import-sort",
"@tanstack/query"
],
"rules": {
"react/jsx-no-bind": [
"error",
{
"ignoreRefs": true,
"allowArrowFunctions": true,
"allowFunctions": false,
"allowBind": false,
"ignoreDOMComponents": true
}
],
"react/jsx-boolean-value": [
"error",
"never",
{
"always": []
}
],
"react/jsx-handler-names": [
"off",
{
"eventHandlerPrefix": "handle",
"eventHandlerPropPrefix": "on"
}
],
"react/no-danger": "warn",
"react/no-deprecated": [
"error"
],
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-no-undef": "error",
"react/jsx-closing-tag-location": "warn",
"react/jsx-no-useless-fragment": "warn",
"react/no-unescaped-entities": "error",
"react/jsx-tag-spacing": [
"error",
{
"closingSlash": "never",
"beforeSelfClosing": "always",
"afterOpening": "never",
"beforeClosing": "never"
}
],
"react/jsx-space-before-closing": [
"off",
"always"
],
"react/jsx-curly-brace-presence": [
"error",
{
"props": "never",
"children": "never"
}
],
"react/destructuring-assignment": [
"error",
"always"
],
"react/no-unsafe": "off",
"react/prop-types": [
"error",
{
"ignore": [],
"customValidators": [],
"skipUndeclared": true
}
],
"simple-import-sort/imports": "warn",
"simple-import-sort/exports": "warn"
},
"overrides": [
// override "simple-import-sort" config
{
"files": [
"*.js",
"*.jsx",
"*.ts",
"*.tsx"
],
"rules": {
"simple-import-sort/imports": [
"error",
{
"groups": [
// Packages `react` related packages come first.
[
"^react",
"^@?\\w"
],
// Internal packages.
[
"^(@|components)(/.*|$)"
],
// Side effect imports.
[
"^\\u0000"
],
// Parent imports. Put `..` last.
[
"^\\.\\.(?!/?$)",
"^\\.\\./?$"
],
// Other relative imports. Put same-folder imports and `.` last.
[
"^\\./(?=.*/)(?!/?$)",
"^\\.(?!/?$)",
"^\\./?$"
],
// Style imports.
[
"^.+\\.?(css)$"
]
]
}
]
}
}
]
}
-5
View File
@@ -1,5 +0,0 @@
{
"$schema": "../../node_modules/oxlint/configuration_schema.json",
"extends": ["../../.oxlintrc.json"],
"plugins": ["unicorn", "typescript", "oxc", "react", "vitest"]
}
+9
View File
@@ -0,0 +1,9 @@
{
"endOfLine": "lf",
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 120
}
+35 -20
View File
@@ -1,22 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="/" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#101010" />
<meta name="ontime" content="ontime - time keeping for live events" />
<link rel="apple-touch-icon" href="/ontime-logo.png" />
<link rel="icon" type="image/png" href="/ontime-logo.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="manifest" href="/manifest.json" />
<meta name="robots" content="noindex" />
<title>ontime</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
<html lang='en'>
<head>
<meta charset='utf-8' />
<link rel='icon' href='/favicon.ico' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta name='theme-color' content='#121212' />
<meta name='ontime' content='ontime - event timer manager' />
<link rel='apple-touch-icon' href='/logo192.png' />
<link
rel='icon'
type='image/png'
sizes='32x32'
href='/favicon-32x32.png'
/>
<link
rel='icon'
type='image/png'
sizes='16x16'
href='/favicon-16x16.png'
/>
<link rel='manifest' href='/site.webmanifest' />
<link rel='manifest' href='/manifest.json' />
<title>ontime</title>
<style>
body,
html {
background-color: rgba(0, 0, 0, 0) !important;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id='root'></div>
<script type='module' src='/src/index.tsx'></script>
</body>
</html>
+59 -39
View File
@@ -1,47 +1,51 @@
{
"name": "ontime-ui",
"version": "4.5.0",
"version": "3.0.0-alpha",
"private": true,
"type": "module",
"dependencies": {
"@base-ui/react": "1.0.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@chakra-ui/react": "^2.7.0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource/open-sans": "^5.2.6",
"@mantine/hooks": "^8.3.7",
"@sentry/react": "^10.2.0",
"@table-nav/react": "^0.0.7",
"@tanstack/react-query": "^5.85.9",
"@tanstack/react-query-devtools": "^5.85.9",
"@tanstack/react-table": "^8.21.3",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mantine/hooks": "^7.6.2",
"@react-icons/all-files": "^4.1.0",
"@sentry/react": "^7.92.0",
"@tanstack/react-query": "^5.17.9",
"@tanstack/react-query-devtools": "^5.17.9",
"@tanstack/react-table": "^8.11.3",
"autosize": "^6.0.1",
"axios": "^1.12.2",
"csv-stringify": "^6.6.0",
"prismjs": "^1.30.0",
"react": "^19.2.3",
"axios": "^1.2.0",
"color": "^4.2.3",
"csv-stringify": "^6.4.5",
"framer-motion": "^10.10.0",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-dom": "^19.2.3",
"react-dom": "^18.2.0",
"react-fast-compare": "^3.2.2",
"react-hook-form": "^7.62.0",
"react-icons": "5.5.0",
"react-qr-code": "^2.0.18",
"react-router": "^7.11.0",
"react-simple-code-editor": "^0.14.1",
"react-virtuoso": "^4.17.0",
"web-vitals": "^5.1.0",
"zustand": "^5.0.9"
"react-hook-form": "^7.49.2",
"react-qr-code": "^2.0.12",
"react-router-dom": "^6.3.0",
"typeface-open-sans": "^1.1.13",
"web-vitals": "^3.1.1",
"zustand": "^4.5.0"
},
"scripts": {
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
"postinstall": "pnpm addversion",
"dev": "cross-env BROWSER=none vite",
"dev:electron": "pnpm dev",
"lint": "oxlint --quiet --type-aware",
"typecheck": "tsc -p tsconfig.app.json --noEmit",
"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",
"test": "vitest",
"test:pipeline": "vitest run",
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build",
"analyse": "npx vite-bundle-visualizer"
},
"browserslist": {
@@ -57,20 +61,36 @@
]
},
"devDependencies": {
"@sentry/vite-plugin": "^2.16.1",
"@types/prismjs": "^1.26.5",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "4.5.1",
"babel-plugin-react-compiler": "1.0.0",
"@sentry/vite-plugin": "^2.14.0",
"@tanstack/eslint-plugin-query": "^5.8.4",
"@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/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",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react": "^7.32.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-testing-library": "^5.9.1",
"jsdom": "^21.1.0",
"ontime-types": "workspace:*",
"ontime-utils": "workspace:*",
"prettier": "^3.0.3",
"sass": "^1.57.1",
"typescript": "catalog:",
"vite": "6.3.1",
"vite-plugin-compression2": "2.2.0",
"vite-plugin-svgr": "4.3.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "catalog:"
"typescript": "^5.2.2",
"vite": "^5.1.0",
"vite-plugin-compression2": "^0.12.0",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.2.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+9 -2
View File
@@ -4,11 +4,18 @@
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "ontime-logo.png",
"type": "image/png"
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"scope": "/",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

+1 -8
View File
@@ -1,8 +1 @@
{
"name": "",
"short_name": "",
"icons": [{ "src": "/ontime-logo.png", "sizes": "295x295", "type": "image/png" }],
"theme_color": "#2B5ABC",
"background_color": "#101010",
"display": "standalone"
}
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
+48 -22
View File
@@ -1,45 +1,71 @@
import { Tooltip } from '@base-ui/react/tooltip';
import { 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 { BrowserRouter } from 'react-router';
// apply global axios config defaults
import './common/api/axios.config';
import AppRouter from './AppRouter';
import { ContextMenu } from './common/components/context-menu/ContextMenu';
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
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 { baseURI } from './externals';
import KeepAwake from './features/keep-awake/KeepAwake';
import theme from './theme/theme';
import { TranslationProvider } from './translation/TranslationProvider';
import AppRouter from './AppRouter';
connectSocket();
// Load Open Sans typeface
// @ts-expect-error no types from font import
import('typeface-open-sans');
const preferredClientName = socketClientName.getState().name;
connectSocket(preferredClientName);
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');
}
}
};
if (isElectron) {
document.addEventListener('keydown', handleKeyPress);
}
return () => {
if (isElectron) {
document.removeEventListener('keydown', handleKeyPress);
}
};
}, [isElectron, sendToElectron]);
return (
<QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider>
<Tooltip.Provider>
<BrowserRouter basename={baseURI}>
<ChakraProvider disableGlobalStyle resetCSS theme={theme}>
<QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider>
<BrowserRouter>
<div className='App'>
<ErrorBoundary>
<TranslationProvider>
<IdentifyOverlay />
<KeepAwake />
<AppRouter />
<ContextMenu>
<AppRouter />
</ContextMenu>
</TranslationProvider>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
<ErrorBoundary>
<div id='identify-portal' />
</ErrorBoundary>
</BrowserRouter>
</Tooltip.Provider>
</AppContextProvider>
</QueryClientProvider>
</AppContextProvider>
</QueryClientProvider>
</ChakraProvider>
);
}
+55 -205
View File
@@ -1,115 +1,68 @@
import { OntimeView, OntimeViewPresettable, URLPreset } from 'ontime-types';
import { ComponentType, Suspense, lazy, useEffect, useMemo } from 'react';
import { Navigate, Route, useLocation, useNavigate, useParams } from 'react-router';
import { lazy, Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import ViewNavigationMenu from './common/components/navigation-menu/ViewNavigationMenu';
import { PresetContext } from './common/context/PresetContext';
import useUrlPresets from './common/hooks-query/useUrlPresets';
import { useClientPath } from './common/hooks/useClientPath';
import { getRouteFromPreset } from './common/utils/urlPresets';
import { getIsNavigationLocked, sessionScope } from './externals';
import Log from './features/log/Log';
import { initializeSentry } from './sentry.config';
import Loader from './views/common/loader/Loader';
import NotFound from './views/common/not-found/NotFound';
import ViewLoader from './views/ViewLoader';
import withPreset from './features/PresetWrapper';
import withData from './features/viewers/ViewWrapper';
const Timer = lazy(() => import('./views/timer/Timer'));
const Countdown = lazy(() => import('./views/countdown/Countdown'));
const Backstage = lazy(() => import('./views/backstage/Backstage'));
const StudioClock = lazy(() => import('./views/studio/Studio'));
const Timeline = lazy(() => import('./views/timeline/TimelinePage'));
const ProjectInfo = lazy(() => import('./views/project-info/ProjectInfo'));
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
const Operator = lazy(() => import('./features/operator/Operator'));
const Editor = lazy(() => import('./views/editor/ProtectedEditor'));
const Cuesheet = lazy(() => import('./views/cuesheet/ProtectedCuesheet'));
const Operator = lazy(() => import('./features/operator/OperatorExport'));
const TimerView = lazy(() => import('./features/viewers/timer/Timer'));
const MinimalTimerView = lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
const ClockView = lazy(() => import('./features/viewers/clock/Clock'));
const Countdown = lazy(() => import('./features/viewers/countdown/Countdown'));
const Backstage = lazy(() => import('./features/viewers/backstage/Backstage'));
const Public = lazy(() => import('./features/viewers/public/Public'));
const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerThird'));
const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock'));
const STimer = withPreset(withData(TimerView));
const SMinimalTimer = withPreset(withData(MinimalTimerView));
const SClock = withPreset(withData(ClockView));
const SCountdown = withPreset(withData(Countdown));
const SBackstage = withPreset(withData(Backstage));
const SPublic = withPreset(withData(Public));
const SLowerThird = withPreset(withData(Lower));
const SStudio = withPreset(withData(StudioClock));
const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper'));
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
const TimerControl = lazy(() => import('./features/control/playback/TimerControlExport'));
const MessageControl = lazy(() => import('./features/control/message/MessageControlExport'));
// Initialize Sentry with our configuration
const SentryRouter = initializeSentry();
export default function AppRouter() {
// handle client path changes
useClientPath();
return (
<Suspense fallback={<Loader />}>
<SentryRouter>
<Suspense fallback={null}>
<Routes>
<Route path='/' element={<Navigate to='/timer' />} />
<Route
path='timer'
element={
<ViewLoader>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
<Timer />
</ViewLoader>
}
/>
<Route
path='countdown'
element={
<ViewLoader>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
<Countdown />
</ViewLoader>
}
/>
<Route
path='backstage'
element={
<ViewLoader>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
<Backstage />
</ViewLoader>
}
/>
<Route
path='studio'
element={
<ViewLoader>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
<StudioClock />
</ViewLoader>
}
/>
<Route
path='timeline'
element={
<ViewLoader>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
<Timeline />
</ViewLoader>
}
/>
<Route
path='info'
element={
<ViewLoader>
<ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />
<ProjectInfo />
</ViewLoader>
}
/>
<Route path='/timer' element={<STimer />} />
<Route path='/minimal' element={<SMinimalTimer />} />
<Route path='/clock' element={<SClock />} />
<Route path='/countdown' element={<SCountdown />} />
<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='/op' element={<Operator />} />
<Route path='/operator' element={<Operator />} />
{/*/!* Protected Routes *!/*/}
<Route path='editor' element={<Editor />} />
<Route path='cuesheet' element={<Cuesheet />} />
<Route
path='op'
element={
<ViewLoader>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} />
<Operator />
</ViewLoader>
}
/>
<Route path='/editor' element={<Editor />} />
<Route path='/cuesheet' element={<Cuesheet />} />
{/*/!* Protected Routes - Elements *!/*/}
<Route
path='rundown'
path='/rundown'
element={
<EditorFeatureWrapper>
<RundownPanel />
@@ -117,7 +70,7 @@ export default function AppRouter() {
}
/>
<Route
path='timercontrol'
path='/timercontrol'
element={
<EditorFeatureWrapper>
<TimerControl />
@@ -125,7 +78,7 @@ export default function AppRouter() {
}
/>
<Route
path='messagecontrol'
path='/messagecontrol'
element={
<EditorFeatureWrapper>
<MessageControl />
@@ -133,119 +86,16 @@ export default function AppRouter() {
}
/>
<Route
path='log'
path='/log'
element={
<EditorFeatureWrapper>
<Log />
</EditorFeatureWrapper>
}
/>
{/**
* If the views are prefixed with the "preset" path, we are in a locked preset
* Locked presets do not expose their parameters
*/}
<Route path='preset/:alias' element={<PresetView />} />
{/**
* If we havent matched any views or presets, we may be in an unlocked preset
* Unlocked presets are unwrapped to expose their target and parameters
*/}
<Route path='*' element={<RedirectPreset />} />
</SentryRouter>
{/*/!* Send to default if nothing found *!/*/}
<Route path='*' element={<STimer />} />
</Routes>
</Suspense>
);
}
const PresetViewMap: Record<OntimeViewPresettable, ComponentType> = {
[OntimeView.Cuesheet]: Cuesheet,
[OntimeView.Operator]: Operator,
[OntimeView.Timer]: Timer,
[OntimeView.Backstage]: Backstage,
[OntimeView.Timeline]: Timeline,
[OntimeView.StudioClock]: StudioClock,
[OntimeView.Countdown]: Countdown,
[OntimeView.ProjectInfo]: ProjectInfo,
};
/**
* This view will mask a configured canonical route
* and inject the preset search parameters to context
* User are not able to configure the parameters locked presets
*/
function PresetView() {
const { data, status } = useUrlPresets();
const { alias } = useParams();
const preset: URLPreset | undefined = useMemo(() => {
if (status === 'pending' || !alias) return;
return data.find((p) => p.alias === alias && p.enabled);
}, [data, status, alias]);
if (status === 'pending') {
return <Loader />;
}
/**
* We need to check the session scope to determine if the user can navigate
* If the user has a global scope, they can navigate freely
* Otherwise, they are locked to the preset view
*/
const showNav = sessionScope === 'rw';
/**
* If we are in a preset path but cannot find a preset, we will need to show a not found page
* This can happen if the preset was deleted or disabled
*/
if (!preset) {
return (
<>
<ViewNavigationMenu isNavigationLocked={!showNav} suppressSettings />
<NotFound />
</>
);
}
/**
* Locked presets do not allow configuration changes
* Whether the user can navigate is determined by the locked param
*
* We inject the preset to the context value for the view to consume
*/
const Component = PresetViewMap[preset.target as OntimeViewPresettable];
return (
<PresetContext value={preset}>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} suppressSettings />
{Component ? <Component /> : <NotFound />}
</PresetContext>
);
}
function RedirectPreset() {
const { data, status } = useUrlPresets();
const navigate = useNavigate();
const location = useLocation();
// checks if we are in a preset path and resolves a destination URL
const destination = useMemo(() => {
if (status === 'pending') return null;
return getRouteFromPreset(location, data);
}, [data, location, status]);
// if we have a destination, we will navigate to it
useEffect(() => {
if (destination) {
navigate(`/${destination}`, { replace: true });
}
}, [destination, navigate]);
if (status === 'pending') {
return <Loader />;
}
return (
<>
<ViewNavigationMenu isNavigationLocked={getIsNavigationLocked()} suppressSettings />
<NotFound />
</>
);
}
-51
View File
@@ -1,51 +0,0 @@
import axios from 'axios';
import { TranslationObject } from 'ontime-types';
import { ontimeQueryClient } from '../../common/queryClient';
import { TRANSLATION, apiEntryUrl, customTranslationsURL } from './constants';
import type { RequestOptions } from './requestOptions';
const assetsPath = `${apiEntryUrl}/assets`;
/**
* HTTP request to get css contents
*/
export async function getCSSContents(options?: RequestOptions): Promise<string> {
const res = await axios.get(`${assetsPath}/css`, { signal: options?.signal });
return res.data;
}
/**
* HTTP request to post css contents
*/
export async function postCSSContents(css: string): Promise<void> {
await axios.post(`${assetsPath}/css`, {
css,
});
}
/**
* HTTP request to restore default css
*/
export async function restoreCSSContents(): Promise<string> {
const res = await axios.post(`${assetsPath}/css/restore`);
return res.data;
}
/**
* HTTP request to get user translation
*/
export async function getUserTranslation(options?: RequestOptions): Promise<TranslationObject> {
const res = await axios.get(customTranslationsURL, { signal: options?.signal });
return res.data;
}
/**
* HTTP request to post user translation
*/
export async function postUserTranslation(translation: TranslationObject): Promise<void> {
await axios.post(`${assetsPath}/translations`, {
translation,
});
await ontimeQueryClient.invalidateQueries({ queryKey: TRANSLATION });
}
-86
View File
@@ -1,86 +0,0 @@
import axios from 'axios';
import type {
Automation,
AutomationDTO,
AutomationOutput,
AutomationSettings,
Trigger,
TriggerDTO,
} from 'ontime-types';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const automationsPath = `${apiEntryUrl}/automations`;
/**
* HTTP request to get the automations settings
*/
export async function getAutomationSettings(options?: RequestOptions): Promise<AutomationSettings> {
const res = await axios.get(automationsPath, { signal: options?.signal });
return res.data;
}
/**
* HTTP request to edit the automations settings
*/
export async function editAutomationSettings(
automationSettings: Partial<AutomationSettings>,
): Promise<AutomationSettings> {
const res = await axios.post(automationsPath, automationSettings);
return res.data;
}
/**
* HTTP request to create a new automation trigger
*/
export async function addTrigger(trigger: TriggerDTO): Promise<Trigger> {
const res = await axios.post(`${automationsPath}/trigger`, trigger);
return res.data;
}
/**
* HTTP request to update an automation trigger
*/
export async function editTrigger(id: string, trigger: Trigger): Promise<Trigger> {
const res = await axios.put(`${automationsPath}/trigger/${id}`, trigger);
return res.data;
}
/**
* HTTP request to delete an automation trigger
*/
export function deleteTrigger(id: string): Promise<void> {
return axios.delete(`${automationsPath}/trigger/${id}`);
}
/**
* HTTP request to create a new automation
*/
export async function addAutomation(automation: AutomationDTO): Promise<Automation> {
const res = await axios.post(`${automationsPath}/automation`, automation);
return res.data;
}
/**
* HTTP request to update a automation
*/
export async function editAutomation(id: string, automation: Automation): Promise<Automation> {
const res = await axios.put(`${automationsPath}/automation/${id}`, automation);
return res.data;
}
/**
* HTTP request to delete a automation
*/
export function deleteAutomation(id: string): Promise<void> {
return axios.delete(`${automationsPath}/automation/${id}`);
}
/**
* HTTP request to test automation output
* The return is irrelevant as we care for the resolution of the promise
*/
export function testOutput(output: AutomationOutput): Promise<void> {
return axios.post(`${automationsPath}/test`, output);
}
@@ -1,8 +0,0 @@
import axios from 'axios';
import { axiosConfig } from './requestTimeouts';
axios.defaults.validateStatus = (status) => {
return status >= 200 && status < 300;
};
axios.defaults.timeout = axiosConfig.shortTimeout;
+20 -15
View File
@@ -1,29 +1,34 @@
import { serverURL } from '../../externals';
// keys in tanstack store
export const APP_INFO = ['appinfo'];
export const APP_SETTINGS = ['appSettings'];
export const APP_VERSION = ['appVersion'];
export const AUTOMATION = ['automation'];
export const CUSTOM_FIELDS = ['customFields'];
export const HTTP_SETTINGS = ['httpSettings'];
export const OSC_SETTINGS = ['oscSettings'];
export const PROJECT_DATA = ['project'];
export const PROJECT_LIST = ['projectList'];
export const PROJECT_RUNDOWNS = ['projectRundowns'];
export const RUNDOWN = ['rundown'];
export const RUNTIME = ['runtimeStore'];
export const SHEET_STATE = ['sheetState'];
export const URL_PRESETS = ['urlpresets'];
export const VIEW_SETTINGS = ['viewSettings'];
export const CLIENT_LIST = ['clientList'];
export const REPORT = ['report'];
export const TRANSLATION = ['translation'];
// API URLs
// resolve location
const location = window.location;
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
export const isProduction = import.meta.env.MODE === 'production';
export const isDev = !isProduction;
// resolve port
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`;
export const apiEntryUrl = `${serverURL}/data`;
const userAssetsPath = 'user';
const cssOverridePath = 'styles/override.css';
const customTranslationsPath = 'translations/translations.json';
export const projectDataURL = `${serverURL}/project`;
export const rundownURL = `${serverURL}/events`;
export const ontimeURL = `${serverURL}/ontime`;
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
export const customTranslationsURL = `${serverURL}/${userAssetsPath}/${customTranslationsPath}`;
export const stylesPath = 'external/styles/override.css';
export const overrideStylesURL = `${serverURL}/${stylesPath}`;
+7 -8
View File
@@ -1,16 +1,15 @@
import axios from 'axios';
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
import { CustomField, CustomFieldLabel, CustomFields } from 'ontime-types';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
/**
* Requests list of known custom fields
*/
export async function getCustomFields(options?: RequestOptions): Promise<CustomFields> {
const res = await axios.get(customFieldsPath, { signal: options?.signal });
export async function getCustomFields(): Promise<CustomFields> {
const res = await axios.get(customFieldsPath);
return res.data;
}
@@ -25,15 +24,15 @@ export async function postCustomField(newField: CustomField): Promise<CustomFiel
/**
* Edits single custom field
*/
export async function editCustomField(key: CustomFieldKey, newField: CustomField): Promise<CustomFields> {
const res = await axios.put(`${customFieldsPath}/${key}`, { ...newField });
export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise<CustomFields> {
const res = await axios.put(`${customFieldsPath}/${label}`, { ...newField });
return res.data;
}
/**
* Deletes single custom field
*/
export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFields> {
const res = await axios.delete(`${customFieldsPath}/${key}`);
export async function deleteCustomField(label: CustomFieldLabel): Promise<CustomFields> {
const res = await axios.delete(`${customFieldsPath}/${label}`);
return res.data;
}
+42 -29
View File
@@ -1,9 +1,15 @@
import axios, { AxiosResponse } from 'axios';
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
import {
DatabaseModel,
GetInfo,
MessageResponse,
ProjectData,
ProjectFileListResponse,
} from 'ontime-types';
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
import { axiosConfig } from './requestTimeouts';
import { createBlob, downloadBlob } from './utils';
const dbPath = `${apiEntryUrl}/db`;
@@ -11,15 +17,15 @@ const dbPath = `${apiEntryUrl}/db`;
/**
* HTTP request to the current DB
*/
export function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
return axios.post(`${dbPath}/download`, { filename }, { timeout: axiosConfig.longTimeout });
async function getDb(): Promise<AxiosResponse<DatabaseModel>> {
return axios.get(`${dbPath}/download`);
}
/**
* Request download of the current project file
* @param fileName
*/
export async function downloadProject(fileName: string) {
export async function downloadProject(fileName: string = 'ontime-project') {
try {
const { data, name } = await fileDownload(fileName);
@@ -32,6 +38,25 @@ export async function downloadProject(fileName: string) {
}
}
/**
* Request download of the current rundown as a CSV file
* @param fileName
*/
export async function downloadCSV(fileName: string = 'rundown') {
try {
const { data, name } = await fileDownload(fileName);
const { project, rundown, customFields } = data;
const sheetData = makeTable(project, rundown, customFields);
const fileContent = makeCSV(sheetData);
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
downloadBlob(blob, `${name}.csv`);
} catch (error) {
console.error(error);
}
}
/**
* HTTP request to upload project file
*/
@@ -39,7 +64,6 @@ export async function uploadProjectFile(file: File): Promise<MessageResponse> {
const formData = new FormData();
formData.append('project', file);
const response = await axios.post(`${dbPath}/upload`, formData, {
timeout: axiosConfig.longTimeout,
headers: {
'Content-Type': 'multipart/form-data',
},
@@ -68,22 +92,11 @@ export async function createProject(
return res.data;
}
/**
* HTTP request to create a project file
*/
export async function quickProject(data: QuickStartData): Promise<MessageResponse> {
const res = await axios.post(`${dbPath}/quick`, data);
return res.data;
}
/**
* HTTP request to get the list of available project files
*/
export async function getProjects(options?: RequestOptions): Promise<ProjectFileListResponse> {
const res = await axios.get(`${dbPath}/all`, {
signal: options?.signal,
timeout: options?.timeout,
});
export async function getProjects(): Promise<ProjectFileListResponse> {
const res = await axios.get(`${dbPath}/all`);
return res.data;
}
@@ -97,14 +110,6 @@ export async function loadProject(filename: string): Promise<MessageResponse> {
return res.data;
}
/**
* HTTP request to load the demo project file
*/
export async function loadDemo(): Promise<MessageResponse> {
const res = await axios.post(`${dbPath}/demo`);
return res.data;
}
/**
* HTTP request to duplicate a project file
*/
@@ -139,13 +144,21 @@ export async function deleteProject(filename: string): Promise<MessageResponse>
return res.data;
}
/**
* HTTP request to retrieve application info
*/
export async function getInfo(): Promise<GetInfo> {
const res = await axios.get(`${dbPath}/info`);
return res.data;
}
/**
* Utility function gets project from db
* @param fileName
* @returns
*/
async function fileDownload(fileName: string): Promise<{ data: DatabaseModel; name: string }> {
const response = await getDb(fileName);
const response = await getDb();
const headerLine = response.headers['Content-Disposition'];
+20 -44
View File
@@ -1,66 +1,42 @@
import axios, { AxiosResponse } from 'axios';
import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
import { CustomFields, OntimeRundown } from 'ontime-types';
import { ImportMap } from 'ontime-utils';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
import { axiosConfig } from './requestTimeouts';
import { downloadBlob } from './utils';
const excelPath = `${apiEntryUrl}/excel`;
type PreviewSpreadsheetResponse = {
rundown: OntimeRundown;
customFields: CustomFields;
};
/**
* upload Excel file to server
* @return string - file ID op the uploaded file
*/
export async function upload(file: File, requestOptions?: RequestOptions): Promise<string[]> {
export async function upload(file: File) {
const formData = new FormData();
formData.append('excel', file);
const response = await axios.post(`${excelPath}/upload`, formData, {
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
await axios.post(`${excelPath}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
}
type PreviewSpreadsheetResponse = {
rundown: Rundown;
customFields: CustomFields;
summary: RundownSummary;
};
export async function importRundownPreview(
options: ImportMap,
requestOptions?: RequestOptions,
): Promise<PreviewSpreadsheetResponse> {
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(
`${excelPath}/preview`,
{
options,
},
{
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
},
);
return response.data;
}
/**
* Downloads a xlsx representation of the rundown from the server
* Get Worksheet names
* @return string[] - array of available worksheets
*/
export async function downloadAsExcel(rundownId: string, fileName?: string, requestOptions?: RequestOptions) {
try {
const response = await axios.get(`${excelPath}/${rundownId}/export`, {
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
responseType: 'blob',
});
downloadBlob(response.data, `${fileName ?? 'Ontime_rundown'}.xlsx`);
} catch (error) {
console.error('Error downloading file:', error);
}
export async function getWorksheetNames(): Promise<string[]> {
const response: AxiosResponse<string[]> = await axios.get(`${excelPath}/worksheets`);
return response.data;
}
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
options,
});
return response.data;
}
+2 -3
View File
@@ -1,7 +1,6 @@
import axios from 'axios';
import { apiRepoLatest } from '../../externals';
import type { RequestOptions } from './requestOptions';
export type HasUpdate = {
url: string;
@@ -11,8 +10,8 @@ export type HasUpdate = {
/**
* HTTP request to get the latest version and url from github
*/
export async function getLatestVersion(options?: RequestOptions): Promise<HasUpdate> {
const res = await axios.get(apiRepoLatest, { signal: options?.signal });
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,
+21
View File
@@ -0,0 +1,21 @@
import axios, { AxiosResponse } from 'axios';
import { HttpSettings } from 'ontime-types';
import { apiEntryUrl } from './constants';
const httpPath = `${apiEntryUrl}/http`;
/**
* HTTP request to retrieve http settings
*/
export async function getHTTP(): Promise<HttpSettings> {
const res = await axios.get(httpPath);
return res.data;
}
/**
* HTTP request to mutate http settings
*/
export async function postHTTP(data: HttpSettings): Promise<AxiosResponse<HttpSettings>> {
return axios.post(httpPath, data);
}
+21
View File
@@ -0,0 +1,21 @@
import axios, { AxiosResponse } from 'axios';
import { OSCSettings } from 'ontime-types';
import { apiEntryUrl } from './constants';
const oscPath = `${apiEntryUrl}/osc`;
/**
* HTTP request to retrieve osc settings
*/
export async function getOSC(): Promise<OSCSettings> {
const res = await axios.get(oscPath);
return res.data;
}
/**
* HTTP request to mutate osc settings
*/
export async function postOSC(data: OSCSettings): Promise<AxiosResponse<OSCSettings>> {
return axios.post(oscPath, data);
}
+3 -24
View File
@@ -1,20 +1,15 @@
import axios, { AxiosResponse } from 'axios';
import { ProjectData, ProjectLogoResponse } from 'ontime-types';
import { ProjectData } from 'ontime-types';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
import { axiosConfig } from './requestTimeouts';
const projectPath = `${apiEntryUrl}/project`;
/**
* HTTP request to fetch project data
*/
export async function getProjectData(options?: RequestOptions): Promise<ProjectData> {
const res = await axios.get(projectPath, {
signal: options?.signal,
timeout: options?.timeout,
});
export async function getProjectData(): Promise<ProjectData> {
const res = await axios.get(projectPath);
return res.data;
}
@@ -24,19 +19,3 @@ export async function getProjectData(options?: RequestOptions): Promise<ProjectD
export async function postProjectData(data: ProjectData): Promise<AxiosResponse<ProjectData>> {
return axios.post(projectPath, data);
}
/**
* HTTP request to upload a project logo
*/
export async function uploadProjectLogo(file: File): Promise<AxiosResponse<ProjectLogoResponse>> {
const formData = new FormData();
formData.append('image', file);
const response = await axios.post(`${projectPath}/upload`, formData, {
timeout: axiosConfig.longTimeout,
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response;
}
-26
View File
@@ -1,26 +0,0 @@
import axios from 'axios';
import { OntimeReport } from 'ontime-types';
import { ontimeQueryClient } from '../../common/queryClient';
import { REPORT, apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
export const reportUrl = `${apiEntryUrl}/report`;
/**
* HTTP request to fetch all reports
*/
export async function fetchReport(options?: RequestOptions): Promise<OntimeReport> {
const res = await axios.get(reportUrl, { signal: options?.signal });
return res.data;
}
export async function deleteReport(id: string) {
await axios.delete(`${reportUrl}/${id}`);
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
}
export async function deleteAllReport() {
await axios.delete(`${reportUrl}/all`);
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
}
@@ -1,4 +0,0 @@
export type RequestOptions = {
signal?: AbortSignal;
timeout?: number;
};
@@ -1,8 +0,0 @@
/**
* Keep a short global timeout for regular API requests, but allow
* longer windows for file transfer and heavy import/export operations.
*/
export const axiosConfig = {
shortTimeout: 20 * 1000, // 20 seconds
longTimeout: 3 * 60 * 1000, // 3 minutes
} as const;
+37 -135
View File
@@ -1,184 +1,86 @@
import axios, { AxiosResponse } from 'axios';
import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types';
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
type RundownId = string;
const rundownPath = `${apiEntryUrl}/rundowns`;
// #region operations on project rundowns =========================
const rundownPath = `${apiEntryUrl}/rundown`;
/**
* HTTP request to fetch a list of existing rundowns
* HTTP request to fetch all events
*/
export async function fetchProjectRundownList(options?: RequestOptions): Promise<ProjectRundownsList> {
const res = await axios.get(rundownPath, { signal: options?.signal });
export async function fetchNormalisedRundown(): Promise<RundownCached> {
const res = await axios.get(`${rundownPath}/normalised`);
return res.data;
}
/**
* HTTP request to fetch all entries in the currently loaded rundown
* HTTP request to post new event
*/
export async function fetchCurrentRundown(options?: RequestOptions): Promise<Rundown> {
const res = await axios.get(`${rundownPath}/current`, { signal: options?.signal });
if (!isValidRundown(res.data)) {
throw new Error('Invalid rundown payload');
}
return res.data;
function isValidRundown(x: any): x is Rundown {
return (
x &&
typeof x === 'object' &&
typeof x.id === 'string' &&
Array.isArray(x.order) &&
Array.isArray(x.flatOrder) &&
x.entries &&
typeof x.entries === 'object' &&
typeof x.revision === 'number'
);
}
export async function requestPostEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
return axios.post(rundownPath, data);
}
/**
* HTTP request to switch the currently loaded rundown
* HTTP request to put new event
*/
export async function loadRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
return axios.post(`${rundownPath}/${rundownId}/load`);
export async function requestPutEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
return axios.put(rundownPath, data);
}
/**
* HTTP request to create a new rundown
*/
export async function createRundown(title: string): Promise<AxiosResponse<ProjectRundownsList>> {
return axios.post(rundownPath, { title });
}
/**
* HTTP request to duplicate an existing rundown
*/
export async function duplicateRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
return axios.post(`${rundownPath}/${rundownId}/duplicate`);
}
/**
* HTTP request to rename an existing rundown
*/
export async function renameRundown(rundownId: RundownId, title: string): Promise<AxiosResponse<ProjectRundownsList>> {
return axios.patch(`${rundownPath}/${rundownId}`, { title });
}
/**
* HTTP request to delete a rundown
*/
export async function deleteRundown(rundownId: RundownId): Promise<AxiosResponse<ProjectRundownsList>> {
return axios.delete(`${rundownPath}/${rundownId}`);
}
// #endregion operations on project rundowns ======================
// #region operations on rundown entries ==========================
/**
* HTTP request to post new entry
*/
export async function postAddEntry(
rundownId: RundownId,
data: TransientEventPayload,
): Promise<AxiosResponse<OntimeEntry>> {
return axios.post(`${rundownPath}/${rundownId}/entry`, data);
}
/**
* HTTP request to edit an entry
*/
export async function putEditEntry(
rundownId: RundownId,
data: Partial<OntimeEntry>,
): Promise<AxiosResponse<OntimeEntry>> {
return axios.put(`${rundownPath}/${rundownId}/entry`, data);
}
export type BatchEditEntry = {
type BatchEditEntry = {
data: Partial<OntimeEvent>;
ids: EntryId[];
ids: string[];
};
/**
* HTTP request to edit multiple events
* HTTP request to put multiple events
*/
export async function putBatchEditEvents(rundownId: RundownId, data: BatchEditEntry): Promise<AxiosResponse<Rundown>> {
return axios.put(`${rundownPath}/${rundownId}/batch`, data);
export async function requestBatchPutEvents(data: BatchEditEntry): Promise<AxiosResponse<MessageResponse>> {
return axios.put(`${rundownPath}/batch`, data);
}
export type ReorderEntry = {
entryId: EntryId;
destinationId: EntryId;
order: 'before' | 'after' | 'insert';
eventId: string;
from: number;
to: number;
};
/**
* HTTP request to reorder an entry
* HTTP request to reorder events
*/
export async function patchReorderEntry(rundownId: RundownId, data: ReorderEntry): Promise<AxiosResponse<Rundown>> {
return axios.patch(`${rundownPath}/${rundownId}/reorder`, data);
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeRundownEntry>> {
return axios.patch(`${rundownPath}/reorder`, data);
}
export type SwapEntry = {
from: string;
to: string;
};
/**
* HTTP request to swap two events
*/
export async function requestEventSwap(
rundownId: RundownId,
from: EntryId,
to: EntryId,
): Promise<AxiosResponse<Rundown>> {
return axios.patch(`${rundownPath}/${rundownId}/swap`, { from, to });
export async function requestEventSwap(data: SwapEntry): Promise<AxiosResponse<MessageResponse>> {
return axios.patch(`${rundownPath}/swap`, data);
}
/**
* HTTP request to request application of delay
*/
export async function requestApplyDelay(rundownId: RundownId, delayId: EntryId): Promise<AxiosResponse<Rundown>> {
return axios.patch(`${rundownPath}/${rundownId}/applydelay/${delayId}`);
export async function requestApplyDelay(eventId: string): Promise<AxiosResponse<MessageResponse>> {
return axios.patch(`${rundownPath}/applydelay/${eventId}`);
}
/**
* HTTP request for cloning an entry
* HTTP request to delete given event
*/
export async function postCloneEntry(
rundownId: RundownId,
entryId: EntryId,
options?: { before?: EntryId; after?: EntryId },
): Promise<AxiosResponse<Rundown>> {
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
export async function requestDelete(eventId: string): Promise<AxiosResponse<MessageResponse>> {
return axios.delete(`${rundownPath}/${eventId}`);
}
/**
* HTTP request for grouping a list of entries into a group
* HTTP request to delete all events
*/
export async function requestGroupEntries(rundownId: RundownId, entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
return axios.post(`${rundownPath}/${rundownId}/group`, { ids: entryIds });
export async function requestDeleteAll(): Promise<AxiosResponse<MessageResponse>> {
return axios.delete(`${rundownPath}/all`);
}
/**
* HTTP request for dissolving of a group
*/
export async function requestUngroup(rundownId: RundownId, groupId: EntryId): Promise<AxiosResponse<Rundown>> {
return axios.post(`${rundownPath}/${rundownId}/ungroup/${groupId}`);
}
/**
* HTTP request to delete entries of a given rundown
*/
export async function deleteEntries(rundownId: RundownId, entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
return axios.delete(`${rundownPath}/${rundownId}/entries`, { data: { ids: entryIds } });
}
/**
* HTTP request to delete all entries of a given rundown
*/
export async function requestDeleteAll(rundownId: RundownId): Promise<AxiosResponse<Rundown>> {
return axios.delete(`${rundownPath}/${rundownId}/all`);
}
// #endregion operations on rundown entries =======================
-23
View File
@@ -1,23 +0,0 @@
import axios from 'axios';
import { GetInfo, LinkOptions } from 'ontime-types';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const sessionPath = `${apiEntryUrl}/session`;
/**
* HTTP request to retrieve application info
*/
export async function getInfo(options?: RequestOptions): Promise<GetInfo> {
const res = await axios.get(`${sessionPath}/info`, { signal: options?.signal });
return res.data;
}
/**
* HTTP request to get a pre-authenticated URL
*/
export async function generateUrl(options: LinkOptions & { baseUrl: string; path: string }): Promise<string> {
const res = await axios.post(`${sessionPath}/url`, options);
return res.data.url;
}
+3 -26
View File
@@ -1,16 +1,15 @@
import axios, { AxiosResponse } from 'axios';
import { PortInfo, Settings } from 'ontime-types';
import { Settings } from 'ontime-types';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const settingsPath = `${apiEntryUrl}/settings`;
/**
* HTTP request to retrieve application settings
*/
export async function getSettings(options?: RequestOptions): Promise<Settings> {
const res = await axios.get(settingsPath, { signal: options?.signal });
export async function getSettings(): Promise<Settings> {
const res = await axios.get(settingsPath);
return res.data;
}
@@ -20,25 +19,3 @@ export async function getSettings(options?: RequestOptions): Promise<Settings> {
export async function postSettings(data: Settings): Promise<AxiosResponse<Settings>> {
return axios.post(settingsPath, data);
}
/**
* Allows setting the welcome modal dialog state from the clients
*/
export async function postShowWelcomeDialog(show: boolean) {
axios.post(`${settingsPath}/welcomedialog`, { show });
}
/**
* HTTP request to retrieve server port
*/
export async function getServerPort(): Promise<PortInfo> {
const res = await axios.get(`${settingsPath}/serverport`);
return res.data;
}
/**
* HTTP request to set server port
*/
export async function postServerPort(serverPort: number): Promise<AxiosResponse<PortInfo>> {
return axios.post(`${settingsPath}/serverport`, { serverPort });
}
+7 -35
View File
@@ -1,10 +1,8 @@
import axios, { AxiosResponse } from 'axios';
import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ontime-types';
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
import { ImportMap } from 'ontime-utils';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
import { axiosConfig } from './requestTimeouts';
const sheetsPath = `${apiEntryUrl}/sheets`;
@@ -25,7 +23,6 @@ export const verifyAuthenticationStatus = async (): Promise<{
export const requestConnection = async (
file: File,
sheetId: string,
requestOptions?: RequestOptions,
): Promise<{
verification_url: string;
user_code: string;
@@ -34,8 +31,6 @@ export const requestConnection = async (
formData.append('client_secret', file);
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
headers: {
'Content-Type': 'multipart/form-data',
},
@@ -58,46 +53,23 @@ export const revokeAuthentication = async (): Promise<{ authenticated: Authentic
export const previewRundown = async (
sheetId: string,
options: ImportMap,
requestOptions?: RequestOptions,
): Promise<{
rundown: Rundown;
rundown: OntimeRundown;
customFields: CustomFields;
summary: RundownSummary;
}> => {
const response = await axios.post(
`${sheetsPath}/${sheetId}/read`,
{ options },
{
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
},
);
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
return response.data;
};
export const getWorksheetNames = async (sheetId: string, requestOptions?: RequestOptions): Promise<string[]> => {
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`, undefined, {
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
});
export const getWorksheetNames = async (sheetId: string): Promise<string[]> => {
const response: AxiosResponse<string[]> = await axios.post(`${sheetsPath}/${sheetId}/worksheets`);
return response.data;
};
/**
* HTTP request to upload the rundown to a google sheet
*/
export const uploadRundown = async (
sheetId: string,
options: ImportMap,
requestOptions?: RequestOptions,
): Promise<void> => {
const response = await axios.post(
`${sheetsPath}/${sheetId}/write`,
{ options },
{
signal: requestOptions?.signal,
timeout: requestOptions?.timeout ?? axiosConfig.longTimeout,
},
);
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => {
const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options });
return response.data;
};
+6 -21
View File
@@ -2,35 +2,20 @@ import axios from 'axios';
import { URLPreset } from 'ontime-types';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
/**
* HTTP request to retrieve all presets
* HTTP request to retrieve aliases
*/
export async function getUrlPresets(options?: RequestOptions): Promise<URLPreset[]> {
const res = await axios.get(urlPresetsPath, { signal: options?.signal });
export async function getUrlPresets(): Promise<URLPreset[]> {
const res = await axios.get(urlPresetsPath);
return res.data;
}
/**
* HTTP request to add a preset
* HTTP request to mutate aliases
*/
export async function postUrlPreset(data: URLPreset): Promise<URLPreset[]> {
return (await axios.post(urlPresetsPath, data)).data;
}
/**
* HTTP request to edit a preset
*/
export async function putUrlPreset(alias: string, data: URLPreset): Promise<URLPreset[]> {
return (await axios.put(`${urlPresetsPath}/${alias}`, data)).data;
}
/**
* HTTP request to delete a preset
*/
export async function deleteUrlPreset(alias: string): Promise<URLPreset[]> {
return (await axios.delete(`${urlPresetsPath}/${alias}`)).data;
export async function postUrlPresets(data: URLPreset[]): Promise<URLPreset[]> {
return axios.post(urlPresetsPath, data);
}
+2 -18
View File
@@ -7,7 +7,7 @@ import { addLog } from '../stores/logger';
import { nowInMillis } from '../utils/time';
/**
* Utility unwrap a potential axios error
* Utility unrwap a potential axios error
* @param error
* @returns
*/
@@ -23,8 +23,6 @@ export function maybeAxiosError(error: unknown) {
}
}
return `${statusText}: ${data}`;
} else if (error instanceof Error) {
return error.message;
} else {
if (typeof error !== 'string') {
return JSON.stringify(error);
@@ -34,21 +32,7 @@ export function maybeAxiosError(error: unknown) {
}
/**
* Utility unwrap a an instance of Error
*/
export function unwrapError(error: unknown) {
if (error instanceof Error) {
return error.message;
} else {
if (typeof error !== 'string') {
return JSON.stringify(error);
}
return error;
}
}
/**
* Utility unwraps a potential axios error and sends to logger
* Utility unrwaps a potential axios error and sends to logger
* @param prepend
* @param error
*/
+7 -11
View File
@@ -1,25 +1,21 @@
import axios from 'axios';
import type { ViewSettings } from 'ontime-types';
import { ViewSettings } from 'ontime-types';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
/**
* HTTP request to fetch view settings
* @returns
* HTTP request to retrieve view settings
*/
export async function getViewSettings(options?: RequestOptions): Promise<ViewSettings> {
const res = await axios.get(viewSettingsPath, { signal: options?.signal });
export async function getView(): Promise<ViewSettings> {
const res = await axios.get(viewSettingsPath);
return res.data;
}
/**
* HTTP request to update view settings
* needs to update entire objects, not just a patch
* HTTP request to mutate view settings
*/
export async function postViewSettings(data: ViewSettings): Promise<ViewSettings> {
const res = await axios.post(viewSettingsPath, data);
return res.data;
export async function postViewSettings(data: ViewSettings) {
return axios.post(viewSettingsPath, data);
}
@@ -0,0 +1,12 @@
@use "../../../theme/mixins" as *;
.link {
@include action-link;
font-size: $inner-section-text-size;
}
.linkIcon {
margin-left: $element-inner-spacing;
display: inline-block;
transform: rotate(45deg);
}
@@ -0,0 +1,26 @@
import { MouseEvent, ReactNode } from 'react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import { openLink } from '../../utils/linkUtils';
import style from './AppLink.module.scss';
interface AppLinkProps {
href: string;
children: ReactNode;
}
export default function AppLink(props: AppLinkProps) {
const { href, children } = props;
const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
openLink(href);
};
return (
<a href='#!' target='_blank' rel='noreferrer' className={style.link} onClick={handleClick}>
{children} <IoArrowUp className={style.linkIcon} />
</a>
);
}
@@ -1,84 +0,0 @@
@use './buttonVariants' as base;
@include base.button-variants;
.baseButton {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5em;
width: fit-content;
padding-inline: 1em;
white-space: nowrap;
border: 1px solid transparent;
border-radius: $component-border-radius-md;
cursor: pointer;
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
&:focus-visible {
outline: 2px solid $blue-500;
outline-offset: 2px;
}
&.loading {
cursor: wait;
.content {
opacity: 0;
}
}
}
.content {
display: flex;
align-items: center;
gap: inherit;
}
.loadingOverlay {
position: absolute;
display: grid;
place-content: center;
}
.spinner {
animation: spin 1s linear infinite;
stroke-dasharray: 8;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
.small {
height: 1.5rem;
font-size: calc(1rem - 3px);
}
.medium {
height: 2rem;
font-size: calc(1rem - 2px);
}
.large {
height: 2.5rem;
font-weight: 600;
}
.xlarge {
height: 3rem;
font-weight: 600;
}
.fluid {
width: 100%;
}
@@ -1,53 +0,0 @@
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { IoEllipseOutline } from 'react-icons/io5';
import { cx } from '../../utils/styleUtils';
import style from './Button.module.scss';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?:
| 'primary'
| 'subtle'
| 'subtle-white'
| 'destructive'
| 'subtle-destructive'
| 'ghosted'
| 'ghosted-white'
| 'ghosted-destructive';
size?: 'small' | 'medium' | 'large' | 'xlarge';
fluid?: boolean;
loading?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, children, variant = 'subtle', size = 'medium', fluid, loading, ...buttonProps }, ref) => {
return (
<button
ref={ref}
className={cx([
style.baseButton,
style[variant],
style[size],
fluid && style.fluid,
loading && style.loading,
className,
])}
type='button'
disabled={loading || buttonProps.disabled}
{...buttonProps}
>
<span className={style.content}>{children}</span>
{loading && (
<div className={style.loadingOverlay}>
<IoEllipseOutline className={style.spinner} />
</div>
)}
</button>
);
},
);
Button.displayName = 'Button';
export default Button;
@@ -1,45 +0,0 @@
@use './buttonVariants' as base;
@include base.button-variants;
.baseIconButton {
aspect-ratio: 1;
display: grid;
place-content: center;
border: 1px solid transparent;
border-radius: $component-border-radius-md;
cursor: pointer;
&:focus-visible {
outline: 2px solid $blue-500;
outline-offset: 2px;
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.small {
height: 1.5rem;
font-size: calc(1rem - 3px);
}
.medium {
height: 2rem;
width: 2rem;
}
.large {
height: 2.5rem;
width: 2.5rem;
}
.xlarge {
height: 3rem;
height: 3rem;
font-size: 1.5rem;
}
@@ -1,37 +0,0 @@
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { cx } from '../../utils/styleUtils';
import style from './IconButton.module.scss';
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?:
| 'primary'
| 'subtle'
| 'subtle-white'
| 'destructive'
| 'subtle-destructive'
| 'ghosted'
| 'ghosted-white'
| 'ghosted-destructive';
size?: 'small' | 'medium' | 'large' | 'xlarge';
}
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
({ className, children, variant = 'subtle', size = 'medium', ...buttonProps }, ref) => {
return (
<button
ref={ref}
className={cx([style.baseIconButton, style[variant], style[size], className])}
type='button'
{...buttonProps}
>
{children}
</button>
);
},
);
IconButton.displayName = 'IconButton';
export default IconButton;
@@ -0,0 +1,96 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Button,
IconButton,
Tooltip,
} from '@chakra-ui/react';
import { IoPowerOutline } from '@react-icons/all-files/io5/IoPowerOutline';
import { useEmitLog } from '../../stores/logger';
interface QuitIconBtnProps {
clickHandler: () => void;
disabled?: boolean;
}
const quitBtnStyle = {
color: '#D20300', // $red-700
borderColor: '#D20300', // $red-700
_focus: { boxShadow: 'none' },
_hover: {
background: '#D20300', // $red-700
color: 'white',
_disabled: {
color: '#D20300', // $red-700
background: 'none',
},
},
_active: {
background: '#9A0000', // $red-1000
color: 'white',
},
variant: 'outline',
isRound: true,
};
export default function QuitIconBtn(props: QuitIconBtnProps) {
const { clickHandler, disabled, ...rest } = props;
const [isOpen, setIsOpen] = useState(false);
const { emitInfo } = useEmitLog();
const onClose = () => setIsOpen(false);
const cancelRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
if (window.process?.type === 'renderer') {
window.ipcRenderer.on('user-request-shutdown', () => {
emitInfo('Shutdown request');
setIsOpen(true);
});
}
}, [emitInfo]);
const handleShutdown = useCallback(() => {
onClose();
clickHandler();
}, [clickHandler]);
return (
<>
<Tooltip label='Quit Application'>
<IconButton
aria-label='Quit Application'
size='lg'
icon={<IoPowerOutline />}
onClick={() => setIsOpen(true)}
isDisabled={disabled}
{...quitBtnStyle}
{...rest}
/>
</Tooltip>
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
Ontime Shutdown
</AlertDialogHeader>
<AlertDialogBody>This will shutdown the Ontime server. Are you sure?</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose} variant='ghost'>
Cancel
</Button>
<Button colorScheme='red' onClick={handleShutdown} ml={3}>
Shutdown
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
);
}
@@ -0,0 +1,17 @@
import { MouseEvent } from 'react';
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
interface TooltipActionBtnProps extends IconButtonProps {
clickHandler: (event: MouseEvent) => void | Promise<void>;
tooltip: string;
openDelay?: number;
}
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} />
</Tooltip>
);
}
@@ -1,144 +0,0 @@
@use '@/theme/themeTokens' as *;
@use '@/theme/viewerDefs' as *;
@mixin button-variants {
.subtle {
background: $gray-1050;
color: $blue-400;
line-height: 1em;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
color: $blue-500;
}
&:active:not(:disabled) {
background: $gray-1100;
border-color: $gray-1250;
}
}
.subtle-white {
background: $gray-1050;
color: $ui-white;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
color: $ui-white;
}
&:active:not(:disabled) {
background: $gray-1100;
border-color: $gray-1250;
}
&:disabled {
background: $gray-1050;
}
}
.primary {
background: $blue-700;
color: $ui-white;
&:hover:not(:disabled):not(:active) {
background: $blue-600;
}
&:active:not(:disabled) {
background: $blue-800;
border-color: $blue-900;
}
&:disabled {
opacity: $viewer-opacity-disabled;
}
}
.destructive {
background: $red-700;
color: $ui-white;
&:hover:not(:disabled):not(:active) {
background: $red-600;
color: $ui-white;
}
&:active:not(:disabled) {
background: $red-800;
border-color: $red-900;
}
}
.subtle-destructive {
background: $gray-1050;
color: $red-400;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
color: $red-500;
}
&:active:not(:disabled) {
background: $gray-1100;
border-color: $gray-1250;
}
}
.ghosted {
background: transparent;
color: $blue-500;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
color: $blue-500;
}
&:active:not(:disabled) {
background: $gray-1100;
border-color: $gray-1250;
}
&:disabled {
opacity: $opacity-disabled;
}
}
.ghosted-white {
background: transparent;
color: $ui-white;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
color: $ui-white;
}
&:active:not(:disabled) {
background: $gray-1100;
border-color: $gray-1250;
}
&:disabled {
opacity: $opacity-disabled;
}
}
.ghosted-destructive {
background: transparent;
color: $red-500;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
color: $red-500;
}
&:active:not(:disabled) {
background: $gray-1100;
border-color: $gray-1250;
}
&:disabled {
opacity: $opacity-disabled;
}
}
}
@@ -1,50 +0,0 @@
.checkbox {
box-sizing: border-box;
display: flex;
width: 1rem;
height: 1rem;
align-items: center;
justify-content: center;
border-radius: $component-border-radius-sm;
&[data-unchecked] {
background-color: $gray-1100;
&:hover:not(:disabled) {
background: $gray-1000;
}
&:active:not(:disabled) {
background: $gray-1200;
}
}
&[data-checked] {
background-color: $blue-500;
&:hover:not(:disabled) {
background: $blue-400;
}
&:active:not(:disabled) {
background: $blue-600;
}
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
&:focus-visible {
outline: 2px solid $blue-500;
outline-offset: 2px;
}
}
.indicator {
display: flex;
color: $ui-white;
&[data-unchecked] {
display: none;
}
}
@@ -1,14 +0,0 @@
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
import { IoCheckmark } from 'react-icons/io5';
import style from './Checkbox.module.scss';
export default function Checkbox(checkboxProps: BaseCheckbox.Root.Props) {
return (
<BaseCheckbox.Root className={style.checkbox} {...checkboxProps}>
<BaseCheckbox.Indicator className={style.indicator}>
<IoCheckmark className={style.icon} />
</BaseCheckbox.Indicator>
</BaseCheckbox.Root>
);
}
@@ -1,24 +0,0 @@
.textEntry {
grid-area: input;
display: flex;
align-items: center;
gap: 0.5rem;
color: $label-gray;
}
.inlineEntry {
display: grid;
grid-template-areas:
'label label'
'input btn';
grid-template-columns: 1fr auto;
column-gap: 0.5rem;
}
.redirect {
grid-area: btn;
}
.label {
color: $label-gray;
}
@@ -1,123 +0,0 @@
import { useState } from 'react';
import { IoArrowForward } from 'react-icons/io5';
import { navigatorConstants } from '../../../viewerConfig';
import useUrlPresets from '../../hooks-query/useUrlPresets';
import { setClientRemote } from '../../hooks/useSocket';
import Button from '../buttons/Button';
import Dialog from '../dialog/Dialog';
import Info from '../info/Info';
import Input from '../input/input/Input';
import AppLink from '../link/app-link/AppLink';
import Select from '../select/Select';
import style from './RedirectClientModal.module.scss';
interface RedirectClientModalProps {
id: string;
name: string;
currentPath: string;
origin: string;
isOpen: boolean;
onClose: () => void;
}
export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) {
const { data } = useUrlPresets();
const [path, setPath] = useState(currentPath);
const [selected, setSelected] = useState('/');
const { setRedirect } = setClientRemote;
const handleRedirect = (newPath: string) => {
if (newPath === '/' || newPath === currentPath) {
return;
}
if (newPath.startsWith('preset-')) {
setRedirect({ target: id, redirect: newPath.slice(7) });
} else {
setRedirect({ target: id, redirect: newPath });
}
onClose();
};
const enabledPresets = data.filter((preset) => preset.enabled);
const viewOptions = [
...navigatorConstants.map((view) => ({
value: `/${view.url}`,
label: view.label,
})),
...enabledPresets.map((preset) => ({
value: `preset-${preset.alias}`,
label: `URL Preset: ${preset.alias}`,
})),
];
return (
<Dialog
isOpen={isOpen}
onClose={onClose}
showCloseButton
showBackdrop
title={`Redirect: ${name}`}
bodyElements={
<>
<Info>
Remotely redirect the client to a different URL. <br />
Either by entering a custom path or selecting a URL Preset.
<br />
<br />
<AppLink search='settings=sharing__presets'>Manage URL Presets</AppLink>
</Info>
<div className={style.inlineEntry}>
<span className={style.label}>Enter custom path</span>
<label className={style.textEntry}>
{origin}
<Input placeholder='eg. /timer' fluid value={path} onChange={(event) => setPath(event.target.value)} />
</label>
<Button
variant='primary'
aria-label='Redirect'
disabled={path === currentPath || path === ''}
className={style.redirect}
onClick={() => handleRedirect(path)}
>
Redirect
<IoArrowForward />
</Button>
</div>
<div>
<span className={style.label}>Select View or URL Preset</span>
<div className={style.inlineEntry}>
<label className={style.textEntry}>
{origin}
<Select
fluid
options={viewOptions}
defaultValue={viewOptions[0].value}
onValueChange={(value) => {
if (value === null) return;
setSelected(value);
}}
disabled={enabledPresets.length === 0}
/>
</label>
<Button
variant='primary'
aria-label='Redirect to preset'
className={style.redirect}
disabled={enabledPresets.length === 0 || selected === '/'}
onClick={() => handleRedirect(selected)}
>
Redirect <IoArrowForward />
</Button>
</div>
</div>
</>
}
/>
);
}
@@ -1,50 +0,0 @@
import { useState } from 'react';
import Button from '../../../common/components/buttons/Button';
import { setClientRemote } from '../../hooks/useSocket';
import Dialog from '../dialog/Dialog';
import Input from '../input/input/Input';
interface RenameClientModalProps {
id: string;
name?: string;
isOpen: boolean;
onClose: () => void;
}
export function RenameClientModal({ id, name: currentName = '', isOpen, onClose }: RenameClientModalProps) {
const [name, setName] = useState(currentName);
const { setClientName } = setClientRemote;
const handleRename = () => {
if (name !== currentName && name !== '') {
setClientName({ target: id, rename: name });
}
onClose();
};
const canSubmit = name !== currentName && name !== '';
return (
<Dialog
isOpen={isOpen}
title={`Rename Client: ${currentName}`}
showCloseButton
onClose={onClose}
bodyElements={
<Input height='large' placeholder='New name' value={name} onChange={(event) => setName(event.target.value)} />
}
footerElements={
<>
<Button variant='subtle' size='large' onClick={onClose}>
Cancel
</Button>
<Button variant='primary' size='large' onClick={handleRename} disabled={!canSubmit}>
Submit
</Button>
</>
}
/>
);
}
@@ -0,0 +1,8 @@
.contextMenuButton {
position: absolute;
}
.contextMenuBackdrop {
position: fixed;
inset: 0;
}
@@ -0,0 +1,98 @@
// logic (with some modifications) culled from:
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
import { ReactElement } from 'react';
import { Menu, MenuButton, MenuGroup, MenuList } from '@chakra-ui/react';
import { IconType } from '@react-icons/all-files';
import { create } from 'zustand';
import { ContextMenuOption } from './ContextMenuOption';
import style from './ContextMenu.module.scss';
type ContextMenuCoords = {
x: number;
y: number;
};
export type OptionWithoutGroup = {
label: string;
isDisabled?: boolean;
icon: IconType;
onClick: () => void;
withDivider?: boolean;
};
export type OptionWithGroup = {
label: string;
group: Omit<OptionWithoutGroup, 'isGroup'>[];
};
export type Option = OptionWithoutGroup | OptionWithGroup;
const isOptionWithGroup = (option: Option): option is OptionWithGroup => 'group' in option;
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 size='sm' 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((option) =>
isOptionWithGroup(option) ? (
<MenuGroup key={option.label} title={option.label}>
{option.group.map((groupOption) => (
<ContextMenuOption key={groupOption.label} {...groupOption} />
))}
</MenuGroup>
) : (
<ContextMenuOption key={option.label} {...option} />
),
)}
</MenuList>
</Menu>
</>
);
};
@@ -0,0 +1,12 @@
import { MenuDivider, MenuItem } from '@chakra-ui/react';
import { OptionWithoutGroup } from './ContextMenu';
export const ContextMenuOption = ({ label, onClick, isDisabled, icon: Icon, withDivider }: OptionWithoutGroup) => (
<>
{withDivider && <MenuDivider />}
<MenuItem icon={<Icon />} onClick={onClick} isDisabled={isDisabled}>
{label}
</MenuItem>
</>
);
@@ -1,40 +0,0 @@
.copytag {
display: flex;
align-items: center;
}
.action {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.label {
border-radius: 3px 0 0 3px;
border: 1px solid $gray-1200;
background-color: $white-3;
padding-inline: 0.5rem 1rem;
line-height: 1em;
display: flex;
align-items: center;
color: $label-gray;
}
.small {
height: 1.5rem;
font-size: calc(1rem - 3px);
}
.medium {
height: 2rem;
font-size: calc(1rem - 2px);
}
.large {
height: 2.5rem;
font-weight: 600;
}
.copy {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

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