Compare commits

...

36 Commits

Author SHA1 Message Date
Carlos Valente 723b1cfa48 Fix: issue with text colour (#214)
* fix: adjust text colour from context
* fix: issue with wrong proptype
2022-10-12 20:13:30 +02:00
Carlos Valente f119fb4529 style: revert input style (#211) 2022-09-28 21:14:54 +02:00
Carlos Valente 562cb769c5 Refactor/vite (#209)
* refactor(vite): add dependencies
* refactor(vite): necessary code migrations
* refactor(vite): convert file to tsx
* refactor(vite): config
* refactor(vite): doctype casing
2022-09-28 21:13:20 +02:00
Carlos Valente d86afee477 Feat/205 (#207)
* feat(text-background): add discrete text background to <Minimal>
* refactor: convert <Minimal> to typescript
* feat(clock): create <Clock> view
* feat(clock): version bump
2022-09-20 21:19:17 +02:00
Carlos Valente 70436059d8 Feat/132 (#198) 2022-09-19 21:15:06 +02:00
Carlos Valente fc0cbf6134 fix(203): issue with userfields re-rendering (#206) 2022-09-17 23:32:22 +02:00
Carlos Valente 3128f5a195 Fix/195 (#200)
* refactor: add typescript dependencies
* chore: update chakra-ui
* refactor: typescript config
* refactor: convert <MenuBar> component to typescript
* refactor: convert <TooltipActionBtn> component to typescript
* refactor: improve UX in file upload
* refactor: upgrade dependencies
* refactor: prepare data provider
* refactor: prevent importing bad fields
* refactor: extract merge to provider
* refactor(upload): parser merges only given fields
* refactor(upload): add event fields to excel
* refactor(upload): improve styling on modal open
* style: improve styling in menu
* style: prevent global pollution
* feat(upload): add upload options
* fix: avoid potential bug in log queue
2022-09-17 23:28:52 +02:00
Carlos Valente 94b03312a6 Refactor/styles (#202)
* style: general micro improvements in editor
* version bump
2022-09-14 21:50:14 +02:00
Carlos Valente b7e758bf24 Chore/ts (#199)
* chore: add typescript and dependencies
* refactor: remove react imports
* refactor: fix entrypoint
* chore: upgrade chakra and deps
* chore: configure eslint
2022-09-14 21:01:58 +02:00
Carlos Valente e40d750673 Doc/downloads (#201) 2022-09-14 15:46:04 +02:00
Carlos Valente b384284175 Refact/feature sockets (#193)
* chore: migrate and upgrade state dependencies
* refactor(eventlist): get data from store
* refactor(messagecontrol): get data from store
* refactor(playbackcontrol): get data from store
* refactor(info): get data from store
* refactor(messagecontrol): handle mutations in hook
* feat(playbackcontrol): get data from store
* refactor(playbackcontrol): handle mutations in hook
* refactor(playbackcontrol): simplify object
* refactor: simplify interfaces
* refactor(InputRow): mixed control + styles
* refactor(socket): cleanup unused endpoints
* refactor(table): prepare table socket endpoint
* refactor(timer): extract timer to own endpoint
* refactor(table): get data from store
* chore: update tests
* refactor(socket): update sockets on cycle
* refactor: remove unnecessary safeguards
2022-09-06 21:11:24 +02:00
Carlos Valente 0e450cb6cb Feat/table part1 (#197)
* feat(csv): export data as csv file
* feat(table): toggle fullscreen
* ux: coordinate tooltip open delay
* feat(excelDates): update tests
* refactor: folder structure
2022-09-04 22:44:03 +02:00
Carlos Valente 0651777055 Feat/161 (#186)
* feat(time): add optional time format
* feat(time): show 12 hour time in stage timer
* feat(time): override locally
* feat(time): show timer in selected format
* feat(time): show 12 hour time in table header
* refactor: extract time formatting into utility
* refactor: migrate datetime library
* refactor(formatTime): centralise time formatting for viewers
* feature(12hour): version bump
2022-08-30 19:04:02 +02:00
Carlos Valente a8ce6f3fc1 Prefeat/companion (#192)
* feat: register missing load event endpoints in websocket
* refactor: rename message socket endpoints
* version bump
2022-08-10 13:02:43 +02:00
Carlos Valente b80ae543a4 Prefeat/skip (#188)
* refactor: refactor size of event list
* refactor: optimise event mutations
* refactor: prepare skip feature
* refactor: update tests
* refactor: simplify type assertion
2022-08-09 14:02:46 +02:00
Carlos Valente 9b5d536378 Refact/project (#189)
* refactor: cleanup project entry point
* refactor: remove unused
* refactor: folder restructure
2022-08-03 23:20:23 +02:00
Carlos Valente 05be0c1e95 Feat/162 (#187)
* feat(key): add user options to minimal timer
* feat(key): register minimal timer in settings
* feat(key): version bump
2022-08-03 23:01:03 +02:00
Carlos Valente 18f3f3643b security(server): disable x-powered-by headers (#181)
* security(server): disable x-powered-by headers
2022-07-23 22:14:50 +02:00
Carlos Valente a757058511 Fix/178 (#179)
* chore(DND): upgrade dependencies
* fix(EditableCell): fix bad prop
* fix(EditableCell): fix reordering bug with hidden columns
2022-07-18 20:17:15 +02:00
Carlos Valente a20909b882 Feat/173 (#177)
* feat/173: update dependencies
* feat/173: migrate to playback requests to POST
* feat/173: HTTP startById and startByIndex
* feat/173: HTTP loadById and loadByIndex
* feat/173: add tests
* feat/173: OSC API
* feat/173: Websocket API
2022-07-13 15:49:59 +02:00
Carlos Valente 91a0d975b3 lint/css config stylelint (#176) 2022-07-11 22:56:35 +02:00
Carlos Valente 97e48fc56e Hotfix/countdown delay (#175)
* hotfix/countdown-delay: fix styling on overran event
* hotfix/countdown-delay: prevent falsy issue
* hotfix/countdown-delay: show delay in countdown
* hotfix/countdown-delay: version bump
2022-07-11 22:04:56 +02:00
Carlos Valente 5c3e9e6a2e hotfix/network: increase network timeout (#170) 2022-07-01 08:02:53 +02:00
Carlos Valente 8ece6b141c hotfix/167: style negative timers (#169) 2022-07-01 00:03:26 +02:00
Carlos Valente 3edb534e90 Hotfix/docker (#168)
* hotfix/docker: missing comma
2022-06-30 23:51:24 +02:00
Carlos Valente a9ee0c3f1d feat/120: prevent reflow (#166) 2022-06-30 23:34:23 +02:00
Carlos Valente f4c5a2bf87 Feat/120 (#164)
* feat/120: calculate delay to single event
* feat/120 Countdown
* feat/120 prevent parsing NaN
* feat/120: version bump
* feat/120: update README.md
* feat/120: show delayed countdown timer
2022-06-30 20:26:01 +02:00
Carlos Valente dcc534f92b add timeout to test CI 2022-06-16 22:44:46 +02:00
Carlos Valente 2ee4986dcf Update build.yml
add timeout to build steps
2022-06-16 22:43:54 +02:00
Carlos Valente c1de3190e9 Fix/155 (#158)
* fix/155: fix css polution on detached views
* fix/155: test detached routes
2022-06-16 14:27:13 +02:00
Carlos Valente 1f001cf188 Feat/153 (#157)
* feat/153: always roll
* feat/153: version bump
2022-06-15 20:37:21 +02:00
Carlos Valente 323d365f1a Fix/147 (#152)
* fix/147: add latest tag to release
* fix/147: add latest tag to docker compose
* fix/148: add timezone example to docker-compose
* docker: version bump
2022-06-13 22:11:17 +02:00
Carlos Valente dda92ba3a8 Refactor imports
* refactor: implement import lint
2022-06-11 23:17:16 +02:00
Carlos Valente c576b6308e Update build.yml 2022-06-08 22:22:03 +02:00
Carlos Valente 5802e7cd03 Update build.yml 2022-06-08 22:14:38 +02:00
Carlos Valente 86957593bb Update build.yml 2022-06-08 22:03:28 +02:00
287 changed files with 10155 additions and 11568 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

+20 -14
View File
@@ -10,6 +10,7 @@ on:
jobs: jobs:
build_mac: build_mac:
runs-on: macOS-latest runs-on: macOS-latest
timeout-minutes: 20
env: env:
CI: '' CI: ''
@@ -22,7 +23,7 @@ jobs:
# React # React
- name: React - Install dependencies - name: React - Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client working-directory: ./client
- name: React - Build project - name: React - Build project
@@ -31,13 +32,13 @@ jobs:
# Node server # Node server
- name: Server - Install dependencies - name: Server - Install dependencies
run: yarn install --frozen-lockfile --production run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src working-directory: ./server/src
# App # App
- name: Electron - Install dependencies - name: Electron - Install dependencies
shell: bash shell: bash
run: yarn install --frozen-lockfile && yarn setdb run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server working-directory: ./server
- name: Electron - Build app - name: Electron - Build app
run: yarn dist-mac run: yarn dist-mac
@@ -53,6 +54,7 @@ jobs:
build_win: build_win:
runs-on: windows-latest runs-on: windows-latest
timeout-minutes: 20
env: env:
CI: '' CI: ''
@@ -65,7 +67,7 @@ jobs:
# React # React
- name: React - Install dependencies - name: React - Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client working-directory: ./client
- name: React - Build project - name: React - Build project
@@ -74,13 +76,13 @@ jobs:
# Node server # Node server
- name: Server - Install dependencies - name: Server - Install dependencies
run: yarn install --frozen-lockfile --production run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src working-directory: ./server/src
# App # App
- name: Electron - Install dependencies - name: Electron - Install dependencies
shell: bash shell: bash
run: yarn install --frozen-lockfile && yarn setdb run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server working-directory: ./server
- name: Electron - Build app - name: Electron - Build app
run: yarn dist-win run: yarn dist-win
@@ -96,6 +98,7 @@ jobs:
build_linux: build_linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20
env: env:
CI: '' CI: ''
@@ -108,7 +111,7 @@ jobs:
# React # React
- name: React - Install dependencies - name: React - Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client working-directory: ./client
- name: React - Build project - name: React - Build project
@@ -117,13 +120,13 @@ jobs:
# Node server # Node server
- name: Server - Install dependencies - name: Server - Install dependencies
run: yarn install --frozen-lockfile --production run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src working-directory: ./server/src
# App # App
- name: Electron - Install dependencies - name: Electron - Install dependencies
shell: bash shell: bash
run: yarn install && yarn setdb run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server working-directory: ./server
- name: Electron - Build app - name: Electron - Build app
run: yarn dist-linux run: yarn dist-linux
@@ -139,6 +142,7 @@ jobs:
publish_docker: publish_docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20
env: env:
CI: '' CI: ''
@@ -153,7 +157,7 @@ jobs:
# React # React
- name: React - Install dependencies - name: React - Install dependencies
run: yarn install run: yarn install --network-timeout 300000
working-directory: ./client working-directory: ./client
- name: React - Build project - name: React - Build project
@@ -162,7 +166,7 @@ jobs:
# Node server # Node server
- name: Server - Install dependencies - name: Server - Install dependencies
run: yarn install --frozen-lockfile --production run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src working-directory: ./server/src
# Login to docker # Login to docker
@@ -171,14 +175,16 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
# Prepare builder
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
# Build and push # Build and push
- name: Build and push - name: Build and push
working-directory: ./
id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
+2 -1
View File
@@ -9,6 +9,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20
env: env:
CI: '' CI: ''
@@ -50,4 +51,4 @@ jobs:
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
working-directory: ./server working-directory: ./server
start: yarn cypress start: yarn cypress
+1
View File
@@ -32,6 +32,7 @@ server/src/preloaded-db/db.json
server/src/models/db.json server/src/models/db.json
TODO.md TODO.md
ontime-db/ ontime-db/
ontime-external/
# vscode stuff # vscode stuff
.vscode/* .vscode/*
+1
View File
@@ -46,5 +46,6 @@
</list> </list>
</option> </option>
</inspection_tool> </inspection_tool>
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
</profile> </profile>
</component> </component>
+23 -15
View File
@@ -7,6 +7,7 @@
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/mac-download.png"/></a> <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/mac-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/win-download.png"/></a> <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/win-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/></a> <a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/></a>
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/dockerhub.png"/></a>
</div> </div>
# Ontime # Ontime
@@ -65,13 +66,14 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
- Public Info - Public Info
- Picture in Picture - Picture in Picture
- Studio Clock - Studio Clock
- [Make your own?](#make-your-own-viewer) - [Make your own?](#make-your-own-viewer)
- [x] Configurable realtime Lower Thirds - [x] Configurable realtime Lower Thirds
- [x] Cuesheets with additional custom fields - [x] Cuesheets with additional custom fields
- [x] Send live messages to different screen types - [x] Send live messages to different screen types
- [x] Ability to differentiate between backstage and public data - [x] Ability to differentiate between backstage and public data
- [x] Manage delays workflow - [x] Manage delays workflow
- [x] Open Sound Control (OSC) Control and Feedback - [x] Open Sound Control (OSC) Control and Feedback
- [x] Integrate with hardware using Companion or one of the APIs
- [x] Roll mode: run independently using the system clock - [x] Roll mode: run independently using the system clock
- [x] Import event list from Excel - [x] Import event list from Excel
- [x] URL Aliases (define configurable aliases to ease onsite setup) - [x] URL Aliases (define configurable aliases to ease onsite setup)
@@ -80,6 +82,8 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
same as app) same as app)
- [x] Multi platform (available on Windows, MacOS and Linux) - [x] Multi platform (available on Windows, MacOS and Linux)
- [x] [Headless run](#headless-run) (run server only, configure from a browser locally) - [x] [Headless run](#headless-run) (run server only, configure from a browser locally)
- [x] [Countdown to anything!](https://cpvalente.gitbook.io/ontime/views/countdown): ability to have
a countdown to any scheduled event
## Unopinionated ## Unopinionated
@@ -106,7 +110,9 @@ Taking advantage of the integrations in Ontime, we currently use Ontime with:
webmodule webmodule
- `OBS`: **render views** using the Browser Module - `OBS`: **render views** using the Browser Module
- `QLab`: trigger ontime using **OSC API** - `QLab`: trigger ontime using **OSC API**
- `Companion`: trigger ontime and manipulate timer 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 ### Make your own viewer
@@ -118,12 +124,16 @@ how to get you started and read the docs about
the [Websocket API](https://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api) the [Websocket API](https://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api)
### Headless run ### Headless run
You can self host and run ontime in a docker image, the run command should: You can self host and run ontime in a docker image, the run command should:
- expose the necessary ports (listen in Dockerfile) - expose the necessary ports (listen in Dockerfile)
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````) - mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
- the image name __getontime/ontime__ - the image name __getontime/ontime__
The docker image is in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime) The docker image is
in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
```bash ```bash
docker pull getontime/ontime docker pull getontime/ontime
``` ```
@@ -148,19 +158,15 @@ There are several features planned in the roadmap. These will be implemented in
friendly order unless there is user demand to bump any of them. friendly order unless there is user demand to bump any of them.
- [ ] HTTP Server (vMix integration) - [ ] HTTP Server (vMix integration)
- [ ] Improvement with event component design - [ ] Improvements in event interface
- [ ] New playback mode - [ ] Moderator view
for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100) - [ ] New playback mode for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
- [ ] Companion module
- [ ] Lower Third Manager - [ ] Lower Third Manager
- [ ] Note only event
- [ ] Reach Schedule: way to speedup timer to meet a deadline - [ ] Reach Schedule: way to speedup timer to meet a deadline
### Issues ### Issues
The app is still in pre-release and there are a few issues, mainly concerning responsiveness in If you come across any bugs, [please open an issue]((https://github.com/cpvalente/ontime/issues/new)). Usually bugs get fixed pretty quickly when reported
different screens. If you run into problems, please open an issue with a screenshot and note your
device and its screen resolution
#### Unsigned App #### Unsigned App
@@ -176,17 +182,19 @@ or in Linux
```Could Not Display "ontime-linux.AppImage``` ```Could Not Display "ontime-linux.AppImage```
You can circumvent this by allowing the execution of the app manually. You can circumvent this by allowing the execution of the app manually.
- In Windows: click more and select "Run Anyway" - In Windows: click more and select "Run Anyway"
- in macOS: after attempting to run the installer, navigate to System Preferences -> Security & Privacy and allow the execution of the app - in macOS: after attempting to run the installer, navigate to System Preferences -> Security &
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing File as a Program Privacy and allow the execution of the app
- 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 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 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, have tips on how to improve this, or would like to sponsor the code signing,
please [open an issue, so we can discuss it](https://github.com/cpvalente/ontime/issues/new) please [open an issue, so we can discuss it](https://github.com/cpvalente/ontime/issues/new)
#### Safari #### Safari
There are some issues with Safari versions lower than 13: There are some issues with Safari versions lower than 13:
+2
View File
@@ -0,0 +1,2 @@
node_modules
build
+68 -7
View File
@@ -1,16 +1,25 @@
{ {
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true,
"node": true
},
"extends": [ "extends": [
"react-app", "eslint:recommended",
"react-app/jest", "plugin:react/recommended",
"plugin:react/recommended" "plugin:@typescript-eslint/recommended",
"eslint-config-prettier"
], ],
"plugins": [ "plugins": [
"react", "react",
"testing-library", "testing-library",
"jest" "simple-import-sort"
], ],
"rules": { "rules": {
"jest/no-mocks-import": "warn",
"no-useless-concat": "warn", "no-useless-concat": "warn",
"prefer-template": "warn", "prefer-template": "warn",
"react/jsx-no-bind": [ "react/jsx-no-bind": [
@@ -41,6 +50,8 @@
"react/no-deprecated": [ "react/no-deprecated": [
"error" "error"
], ],
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-no-undef": "error", "react/jsx-no-undef": "error",
"react/jsx-closing-tag-location": "warn", "react/jsx-closing-tag-location": "warn",
"react/jsx-no-useless-fragment": "warn", "react/jsx-no-useless-fragment": "warn",
@@ -77,6 +88,56 @@
"customValidators": [], "customValidators": [],
"skipUndeclared": true "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)$"
]
]
}
]
}
}
]
} }
+6
View File
@@ -0,0 +1,6 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-prettier"
]
}
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<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.jsx'></script>
</body>
</html>
-6
View File
@@ -1,6 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
+44 -20
View File
@@ -1,38 +1,41 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "1.1.1", "version": "1.8.2",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.0.0-next.3", "@chakra-ui/react": "^2.3.2",
"@dnd-kit/core": "^5.0.3", "@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^6.0.1", "@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.1.0", "@dnd-kit/utilities": "^3.2.0",
"@emotion/react": "^11.7.1", "@emotion/react": "^11.10.4",
"@emotion/styled": "^11.6.0", "@emotion/styled": "^11.10.4",
"@react-icons/all-files": "^4.1.0", "@react-icons/all-files": "^4.1.0",
"@tanstack/react-query": "^4.1.3",
"@tanstack/react-query-devtools": "^4.0.10",
"autosize": "^5.0.1", "autosize": "^5.0.1",
"axios": "^0.25.0", "axios": "^0.27.2",
"color": "^4.2.3", "color": "^4.2.3",
"framer-motion": "^6.3.3", "framer-motion": "^7.3.2",
"jotai": "^1.7.8",
"luxon": "^3.0.1",
"react": "^18.1.0", "react": "^18.1.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-qr-code": "^2.0.5", "react-qr-code": "^2.0.5",
"react-query": "^3.38.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",
"socket.io-client": "^4.5.0", "socket.io-client": "^4.5.1",
"typeface-open-sans": "^1.1.13", "typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "set BROWSER=none&&react-scripts start", "start": "set BROWSER=none&&vite",
"build": "react-scripts build", "build": "vite build",
"test": "react-scripts test", "lint": "eslint .",
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ", "stylelint": "npx stylelint \"**/*.scss\"\n",
"eject": "react-scripts eject" "test": "vitest ",
"test:pipeline": "vitest run"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@@ -51,9 +54,30 @@
"@testing-library/react": "^13.1.1", "@testing-library/react": "^13.1.1",
"@testing-library/react-hooks": "^8.0.0", "@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^14.1.1", "@testing-library/user-event": "^14.1.1",
"@types/node": "^18.7.16",
"@types/react": "^18.0.19",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"@vitejs/plugin-react": "^2.1.0",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.0.4",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-testing-library": "^5.6.4",
"jsdom": "^20.0.0",
"prettier": "^2.7.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react-scripts": "^5.0.1",
"react-test-renderer": "^18.1.0", "react-test-renderer": "^18.1.0",
"sass": "^1.51.0" "sass": "^1.51.0",
"stylelint": "^14.9.1",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard-scss": "^4.0.0",
"typescript": "^4.8.3",
"vite": "^3.1.1",
"vite-plugin-svgr": "^2.2.1",
"vite-tsconfig-paths": "^3.5.0",
"vitest": "^0.23.2"
} }
} }
-59
View File
@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#121212" />
<meta name="onsite" content="onsite - event timer manager" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<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>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+30 -113
View File
@@ -1,42 +1,20 @@
import React, { lazy, Suspense, useCallback, useEffect } from 'react'; import { Suspense, useCallback, useEffect } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import './App.scss'; import { ChakraProvider } from '@chakra-ui/react';
import withSocket from 'features/viewers/ViewWrapper'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary'; import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import { useFetch } from './app/hooks/useFetch';
import { ALIASES } from './app/api/apiConstants';
import { getAliases } from './app/api/ontimeApi';
const Editor = lazy(() => import('features/editors/ProtectedEditor')); import { AppContextProvider } from './common/context/AppContext';
const Table = lazy(() => import('features/table/ProtectedTable')); import SocketProvider from './common/context/socketContext';
import theme from './theme/theme';
import AppRouter from './AppRouter';
const TimerView = lazy(() => import('features/viewers/timer/Timer')); // Load Open Sans typeface
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer')); import('typeface-open-sans');
export const ontimeQueryClient = new QueryClient();
const StageManager = lazy(() => import('features/viewers/backstage/StageManager'));
const Public = lazy(() => import('features/viewers/foh/Public'));
const Lower = lazy(() => import('features/viewers/production/lower/LowerWrapper'));
const Pip = lazy(() => import('features/viewers/production/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
const STimer = withSocket(TimerView);
const SMinimalTimer = withSocket(MinimalTimerView);
const SStageManager = withSocket(StageManager);
const SPublic = withSocket(Public);
const SLowerThird = withSocket(Lower);
const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
const FeatureWrapper = lazy(() => import('features/FeatureWrapper'));
const EventList = lazy(() => import('features/editors/list/EventListExport'));
const TimerControl = lazy(() => import('features/control/playback/TimerControlExport'));
const MessageControl = lazy(() => import('features/control/message/MessageControlExport'));
const Info = lazy(() => import('features/info/InfoExport'));
function App() { function App() {
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
const navigate = useNavigate();
// Handle keyboard shortcuts // Handle keyboard shortcuts
const handleKeyPress = useCallback((e) => { const handleKeyPress = useCallback((e) => {
@@ -64,86 +42,25 @@ function App() {
}; };
}, [handleKeyPress]); }, [handleKeyPress]);
// navigate if is alias route
useEffect(() => {
if (data == null) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
break;
}
}
}, [data, location, navigate]);
return ( return (
<div className='App'> <ChakraProvider resetCSS theme={theme}>
<ErrorBoundary> <SocketProvider>
<Suspense fallback={null}> <QueryClientProvider client={ontimeQueryClient}>
<Routes> <AppContextProvider>
<Route path='/' element={<STimer />} /> <BrowserRouter>
<Route path='/speaker' element={<STimer />} /> <div className='App'>
<Route path='/presenter' element={<STimer />} /> <ErrorBoundary>
<Route path='/stage' element={<STimer />} /> <Suspense fallback={null}>
<Route path='/timer' element={<STimer />} /> <AppRouter />
</Suspense>
<Route path='/minimal' element={<SMinimalTimer />} /> </ErrorBoundary>
<Route path='/minimalTimer' element={<SMinimalTimer />} /> <ReactQueryDevtools initialIsOpen={false} />
<Route path='/simpleTimer' element={<SMinimalTimer />} /> </div>
</BrowserRouter>
<Route path='/sm' element={<SStageManager />} /> </AppContextProvider>
<Route path='/backstage' element={<SStageManager />} /> </QueryClientProvider>
</SocketProvider>
<Route path='/public' element={<SPublic />} /> </ChakraProvider>
<Route path='/pip' element={<SPip />} />
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
{/*/!* Protected Routes *!/*/}
<Route path='/editor' element={<Editor />} />
<Route path='/cuesheet' element={<Table />} />
<Route path='/cuelist' element={<Table />} />
<Route path='/table' element={<Table />} />
{/*/!* Protected Routes - Elements *!/*/}
<Route
path='/eventlist'
element={
<FeatureWrapper>
<EventList />
</FeatureWrapper>
}
/>
<Route
path='/timercontrol'
element={
<FeatureWrapper>
<TimerControl />
</FeatureWrapper>
}
/>
<Route
path='/messagecontrol'
element={
<FeatureWrapper>
<MessageControl />
</FeatureWrapper>
}
/>
<Route
path='/info'
element={
<FeatureWrapper>
<Info />
</FeatureWrapper>
}
/>
{/* Send to default if nothing found */}
<Route path='*' element={<STimer />} />
</Routes>
</Suspense>
</ErrorBoundary>
</div>
); );
} }
-45
View File
@@ -1,45 +0,0 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scrollbar-color: rgba(255, 255, 255, 0.15) rgba(255, 255, 255, 0.01);
scrollbar-width: thin;
}
body,
html,
.App {
margin: 0 auto;
overflow: hidden;
overflow: clip;
height: 100vh;
-webkit-user-select: none;
user-select: none;
-webkit-app-region: drag;
}
option {
color: black;
}
/* width */
::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
}
/* Track */
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.01);
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.35);
}
+125
View File
@@ -0,0 +1,125 @@
import { lazy, useEffect } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { ALIASES } from './common/api/apiConstants';
import { getAliases } from './common/api/ontimeApi';
import { useFetch } from './common/hooks/useFetch';
import { useSocketProvider } from './common/hooks/useSocketProvider';
import withSocket from './features/viewers/ViewWrapper';
const Editor = lazy(() => import('features/editors/ProtectedEditor'));
const Table = lazy(() => import('features/table/ProtectedTable'));
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/LowerWrapper'));
const Pip = lazy(() => import('features/viewers/picture-in-picture/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
const STimer = withSocket(TimerView);
const SMinimalTimer = withSocket(MinimalTimerView);
const SClock = withSocket(ClockView);
const SCountdown = withSocket(Countdown);
const SBackstage = withSocket(Backstage);
const SPublic = withSocket(Public);
const SLowerThird = withSocket(Lower);
const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
const FeatureWrapper = lazy(() => import('features/FeatureWrapper'));
const EventList = lazy(() => import('features/editors/list/EventListExport'));
const TimerControl = lazy(() => import('features/control/playback/TimerControlExport'));
const MessageControl = lazy(() => import('features/control/message/MessageControlExport'));
const Info = lazy(() => import('features/info/InfoExport'));
export default function AppRouter() {
useSocketProvider();
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
const navigate = useNavigate();
// navigate if is alias route
useEffect(() => {
if (!data) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
break;
}
}
}, [data, location, navigate]);
return(
<Routes>
<Route path='/' element={<STimer />} />
<Route path='/speaker' element={<STimer />} />
<Route path='/presenter' element={<STimer />} />
<Route path='/stage' element={<STimer />} />
<Route path='/timer' element={<STimer />} />
<Route path='/minimal' element={<SMinimalTimer />} />
<Route path='/minimalTimer' element={<SMinimalTimer />} />
<Route path='/simpleTimer' element={<SMinimalTimer />} />
<Route path='/clock' element={<SClock />} />
<Route path='/countdown' element={<SCountdown />} />
<Route path='/sm' element={<SBackstage />} />
<Route path='/backstage' element={<SBackstage />} />
<Route path='/public' element={<SPublic />} />
<Route path='/pip' element={<SPip />} />
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
{/*/!* Protected Routes *!/*/}
<Route path='/editor' element={<Editor />} />
<Route path='/cuesheet' element={<Table />} />
<Route path='/cuelist' element={<Table />} />
<Route path='/table' element={<Table />} />
{/*/!* Protected Routes - Elements *!/*/}
<Route
path='/eventlist'
element={
<FeatureWrapper>
<EventList />
</FeatureWrapper>
}
/>
<Route
path='/timercontrol'
element={
<FeatureWrapper>
<TimerControl />
</FeatureWrapper>
}
/>
<Route
path='/messagecontrol'
element={
<FeatureWrapper>
<MessageControl />
</FeatureWrapper>
}
/>
<Route
path='/info'
element={
<FeatureWrapper>
<Info />
</FeatureWrapper>
}
/>
{/* Send to default if nothing found */}
<Route path='*' element={<STimer />} />
</Routes>
)
}
+2 -2
View File
@@ -1,4 +1,4 @@
import {QueryClient} from "react-query"; import { QueryClient } from '@tanstack/react-query';
export const queryClientMock = new QueryClient({ export const queryClientMock = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -6,4 +6,4 @@ export const queryClientMock = new QueryClient({
retry: false, retry: false,
}, },
}, },
}); });
-25
View File
@@ -1,25 +0,0 @@
export const STATIC_PORT = 4001;
export const EVENT_TABLE = 'event';
export const ALIASES = 'aliases';
export const USERFIELDS = 'userFields';
export const EVENTS_TABLE = 'events';
export const APP_TABLE = 'appinfo';
export const OSC_SETTINGS = 'oscSettings';
export const APP_SETTINGS = 'appSettings';
/**
* @description finds server path given the current location, it
* @return {*}
*/
const calculateServer = () => {
if (process.env?.NODE_ENV === 'development') {
return `http://localhost:${STATIC_PORT}`;
}
return window.location.origin;
};
export const serverURL = calculateServer();
export const eventURL = `${serverURL}/${EVENT_TABLE}`;
export const eventsURL = `${serverURL}/${EVENTS_TABLE}`;
export const playbackURL = `${serverURL}/playback`;
export const ontimeURL = `${serverURL}/ontime`;
@@ -1,32 +0,0 @@
import React, { createContext, useState } from 'react';
export const LocalEventSettingsContext = createContext({
showQuickEntry: false,
starTimeIsLastEnd: true,
defaultPublic: true,
setShowQuickEntry: () => undefined,
setStarTimeIsLastEnd: () => undefined,
setDefaultPublic: () => undefined,
});
export const LocalEventSettingsProvider = ({ children }) => {
const [showQuickEntry, setShowQuickEntry] = useState(false);
const [starTimeIsLastEnd, setStarTimeIsLastEnd] = useState(true);
const [defaultPublic, setDefaultPublic] = useState(false);
return (
<LocalEventSettingsContext.Provider
value={{
showQuickEntry,
setShowQuickEntry,
starTimeIsLastEnd,
setStarTimeIsLastEnd,
defaultPublic,
setDefaultPublic,
}}
>
{children}
</LocalEventSettingsContext.Provider>
);
};
-23
View File
@@ -1,23 +0,0 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import io from 'socket.io-client';
import { serverURL } from 'app/api/apiConstants';
const SocketContext = createContext([[], () => {}]);
export const useSocket = () => {
return useContext(SocketContext);
};
function SocketProvider({ children }) {
const [socket, setSocket] = useState();
useEffect(() => {
const s = io(serverURL, { transports: ['websocket'] });
setSocket(s);
return () => s.disconnect();
}, []);
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
}
export default SocketProvider;
-52
View File
@@ -1,52 +0,0 @@
const dummy = new Date();
export const sampleData = {
presenterMessage: {
text: 'Only the timer sees this',
active: false,
},
publicMessage: {
text: 'Everyone sees this',
active: false,
},
events: [
{
id: '1',
title: 'Is the internet a fad?',
subtitle: 'It is',
presenter: 'Carlos Valente',
timeStart: 37800000,
timeEnd: 39600000,
clockStarted: dummy,
timerDuration: 60,
type: 'event',
},
{
id: 0.4849093424577693,
duration: 25*60000,
type: 'delay',
},
{
id: '2',
title: 'Is reddit a dictatorship?',
subtitle: 'It is',
presenter: 'Carlos Valente',
timeStart: 39600000,
timeEnd: 40320000,
clockStarted: dummy,
timerDuration: 60*60000,
type: 'event',
},
{
id: '3',
title: 'Out of words',
subtitle: '',
presenter: 'Carlos Valente',
timeStart: 41400000,
timeEnd: 43200000,
clockStarted: dummy,
timerDuration: 60,
type: 'event',
},
],
};
@@ -1,4 +1,5 @@
// Exported viewer link location // Exported viewer link location
const minimalLocation = 'minimal';
const speakerLocation = 'speaker'; const speakerLocation = 'speaker';
const smLocation = 'sm'; const smLocation = 'sm';
const publicLocation = 'public'; const publicLocation = 'public';
@@ -7,6 +8,7 @@ const studioLocation = 'studio';
const cuesheetLocation = 'cuesheet'; const cuesheetLocation = 'cuesheet';
export const viewerLocations = [ export const viewerLocations = [
{ link: minimalLocation, label: 'Minimal timer' },
{ link: speakerLocation, label: 'Speaker Screen' }, { link: speakerLocation, label: 'Speaker Screen' },
{ link: smLocation, label: 'Backstage Screen' }, { link: smLocation, label: 'Backstage Screen' },
{ link: publicLocation, label: 'Public Screen' }, { link: publicLocation, label: 'Public Screen' },
+32
View File
@@ -0,0 +1,32 @@
export const STATIC_PORT = 4001;
export const EVENT_TABLE = ['event'];
export const ALIASES = ['aliases'];
export const USERFIELDS = ['userFields'];
export const EVENTS_TABLE = ['events'];
export const APP_TABLE = ['appinfo'];
export const OSC_SETTINGS = ['oscSettings'];
export const APP_SETTINGS = ['appSettings'];
export const VIEW_SETTINGS = ['viewSettings'];
export const FEAT_EVENTLIST = ['feat-eventList'];
export const FEAT_MESSAGECONTROL = ['feat-messagecontrol'];
export const FEAT_PLAYBACKCONTROL = ['feat-playbackcontrol'];
export const FEAT_INFO = ['feat-info'];
export const FEAT_CUESHEET = ['feat-cuesheet'];
export const TIMER = ['ontime-timer'];
/**
* @description finds server path given the current location, it
* @return {*}
*/
export const calculateServer = () =>
import.meta.env.DEV ? `http://localhost:${STATIC_PORT}` : window.location.origin;
export const serverURL = calculateServer();
export const eventURL = `${serverURL}/${EVENT_TABLE}`;
export const eventsURL = `${serverURL}/${EVENTS_TABLE}`;
export const playbackURL = `${serverURL}/playback`;
export const ontimeURL = `${serverURL}/ontime`;
export const stylesPath = 'external/styles/override.css';
export const overrideStylesURL = `serverURL/${stylesPath}`;
@@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { eventURL } from './apiConstants'; import { eventURL } from './apiConstants';
/** /**
@@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { eventsURL } from './apiConstants'; import { eventsURL } from './apiConstants';
/** /**
@@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { ontimeURL } from './apiConstants'; import { ontimeURL } from './apiConstants';
/** /**
@@ -18,7 +19,12 @@ export const ontimePlaceholderInfo = {
* @type {{pinCode: null}} * @type {{pinCode: null}}
*/ */
export const ontimePlaceholderSettings = { export const ontimePlaceholderSettings = {
app: 'ontime',
version: 1,
serverPort: 4001,
lock: null,
pinCode: null, pinCode: null,
timeFormat: '24',
}; };
/** /**
@@ -61,6 +67,14 @@ export const oscPlaceholderSettings = {
enabled: false, enabled: false,
}; };
/**
* @description placeholder information for view settings
* @type {{overrideCSS: boolean}}
*/
export const viewsPlaceholder = {
overrideStyles: false,
};
/** /**
* @description placeholder information for httpSettings * @description placeholder information for httpSettings
* @type {{onStart: {url: string, enabled: boolean}, onLoad: {url: string, enabled: boolean}, onPause: {url: string, enabled: boolean}, onFinish: {url: string, enabled: boolean}, onUpdate: {url: string, enabled: boolean}, onStop: {url: string, enabled: boolean}}} * @type {{onStart: {url: string, enabled: boolean}, onLoad: {url: string, enabled: boolean}, onPause: {url: string, enabled: boolean}, onFinish: {url: string, enabled: boolean}, onUpdate: {url: string, enabled: boolean}, onStop: {url: string, enabled: boolean}}}
@@ -157,6 +171,21 @@ export const getInfo = async () => {
*/ */
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data); export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
/**
* @description HTTP request to retrieve view settings
* @return {Promise}
*/
export const getView = async () => {
const res = await axios.get(`${ontimeURL}/views`);
return res.data;
};
/**
* @description HTTP request to mutate view settings
* @return {Promise}
*/
export const postView = async (data) => axios.post(`${ontimeURL}/views`, data);
/** /**
* @description HTTP request to retrieve aliases * @description HTTP request to retrieve aliases
* @return {Promise} * @return {Promise}
@@ -222,9 +251,7 @@ export const downloadEvents = async () => {
filename = headerLine.substring(startFileNameIndex, endFileNameIndex); filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
} }
const url = window.URL.createObjectURL( const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
new Blob([response.data], { type: 'application/json' })
);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.setAttribute('download', filename); link.setAttribute('download', filename);
@@ -237,18 +264,26 @@ export const downloadEvents = async () => {
* @description HTTP request to upload events db * @description HTTP request to upload events db
* @return {Promise} * @return {Promise}
*/ */
export const uploadEvents = async (file) => { export const uploadEvents = async (file, setProgress, options) => {
const formData = new FormData(); const formData = new FormData();
formData.append('userFile', file); // appending file formData.append('userFile', file);
await axios.post(`${ontimeURL}/db`, formData, { const onlyEvents = options?.onlyEvents;
headers: { await axios
'Content-Type': 'multipart/form-data', .post(`${ontimeURL}/db?onlyEvents=${onlyEvents}`, formData, {
}, headers: {
}); 'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
const complete = Math.round((progressEvent.loaded * 100) / progressEvent.total);
setProgress(complete);
},
})
.then((response) => response.data.id);
}; };
/** /**
* @description HTTP request to upload events * @description HTTP request to upload events
* @return {Promise} * @return {Promise}
*/ */
export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath }); export const uploadEventsWithPath = async (filepath) =>
axios.post(`${ontimeURL}/dbpath`, { path: filepath });
@@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { playbackURL } from './apiConstants'; import { playbackURL } from './apiConstants';
/** /**
@@ -0,0 +1,20 @@
import { atomWithStorage, selectAtom } from 'jotai/utils';
export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', {
showQuickEntry: false,
startTimeIsLastEnd: false,
defaultPublic: false,
});
export const showQuickEntryAtom = selectAtom(
eventSettingsAtom,
(settings) => settings.showQuickEntry
);
export const startTimeIsLastEndAtom = selectAtom(
eventSettingsAtom,
(settings) => settings.startTimeIsLastEnd
);
export const defaultPublicAtom = selectAtom(
eventSettingsAtom,
(settings) => settings.defaultPublic
);
@@ -1,12 +1,13 @@
import React from 'react';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiClock } from '@react-icons/all-files/fi/FiClock'; import { FiClock } from '@react-icons/all-files/fi/FiClock';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle'; import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus'; import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function ActionButtons(props) { export default function ActionButtons(props) {
const { showAdd, showDelay, showBlock, actionHandler } = props; const { showAdd, showDelay, showBlock, actionHandler } = props;
@@ -17,7 +18,7 @@ export default function ActionButtons(props) {
return ( return (
<Menu isLazy lazyBehavior='unmount'> <Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add ...' delay={500}> <Tooltip label='Add ...' delay={tooltipDelayMid}>
<MenuButton <MenuButton
as={IconButton} as={IconButton}
aria-label='Options' aria-label='Options'
@@ -1,7 +1,6 @@
import React from 'react';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { Button } from '@chakra-ui/button'; import { Button } from '@chakra-ui/button';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
export default function EnableBtn(props) { export default function EnableBtn(props) {
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function PauseIconBtn(props) { export default function PauseIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props; const { clickhandler, active, disabled, ...rest } = props;
return ( return (
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}> <Tooltip label='Pause timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton <IconButton
icon={<IoPause size='24px' />} icon={<IoPause size='24px' />}
colorScheme='orange' colorScheme='orange'
@@ -1,4 +1,3 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers'; import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Button, IconButton } from '@chakra-ui/button'; import { Button, IconButton } from '@chakra-ui/button';
import { import {
AlertDialog, AlertDialog,
@@ -11,7 +11,8 @@ import {
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
import { FiPower } from '@react-icons/all-files/fi/FiPower'; import { FiPower } from '@react-icons/all-files/fi/FiPower';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { LoggingContext } from '../../context/LoggingContext';
export default function QuitIconBtn(props) { export default function QuitIconBtn(props) {
const { clickHandler, size = 'lg', ...rest } = props; const { clickHandler, size = 'lg', ...rest } = props;
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function RollIconBtn(props) { export default function RollIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props; const { clickhandler, active, disabled, ...rest } = props;
return ( return (
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}> <Tooltip label='Roll mode' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton <IconButton
icon={<IoTimeOutline size='24px' />} icon={<IoTimeOutline size='24px' />}
colorScheme='blue' colorScheme='blue'
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function StartIconBtn(props) { export default function StartIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props; const { clickhandler, active, disabled, ...rest } = props;
return ( return (
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}> <Tooltip label='Start timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton <IconButton
icon={<IoPlay size='24px' />} icon={<IoPlay size='24px' />}
colorScheme='green' colorScheme='green'
@@ -1,27 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
export default function TooltipActionBtn(props) {
const { clickHandler, icon, color, size='xs', tooltip, ...rest } = props;
return (
<Tooltip label={tooltip}>
<IconButton
aria-label={tooltip}
size={size}
icon={icon}
onClick={clickHandler}
{...rest}
/>
</Tooltip>
);
}
TooltipActionBtn.propTypes = {
clickHandler: PropTypes.func,
icon: PropTypes.element,
color: PropTypes.string,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
tooltip: PropTypes.string
}
@@ -0,0 +1,25 @@
import { IconButton } from '@chakra-ui/button';
import { IconButtonProps } from '@chakra-ui/react';
import { Tooltip } from '@chakra-ui/tooltip';
interface TooltipActionBtnProps extends IconButtonProps {
clickHandler: () => 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}
aria-label={tooltip}
size={size}
icon={icon}
onClick={clickHandler}
className={className}
/>
</Tooltip>
);
}
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -1,12 +1,13 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function TransportIconBtn(props) { export default function TransportIconBtn(props) {
const { clickHandler, icon, tooltip, disabled, ...rest } = props; const { clickHandler, icon, tooltip, disabled, ...rest } = props;
return ( return (
<Tooltip label={tooltip} openDelay={500} shouldWrapChildren={disabled}> <Tooltip label={tooltip} openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton <IconButton
icon={icon} icon={icon}
colorScheme='white' colorScheme='white'
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function UnloadIconBtn(props) { export default function UnloadIconBtn(props) {
const { clickHandler, disabled, ...rest } = props; const { clickHandler, disabled, ...rest } = props;
return ( return (
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}> <Tooltip label='Unload event' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton <IconButton
icon={<IoStop size='22px' />} icon={<IoStop size='22px' />}
colorScheme='red' colorScheme='red'
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Icon } from '@chakra-ui/react'; import { Icon } from '@chakra-ui/react';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp'; import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import PropTypes from 'prop-types';
import style from './CollapseBar.module.scss'; import style from './CollapseBar.module.scss';
export default function CollapseBar(props) { export default function CollapseBar(props) {
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *; @use '../../../theme/main' as *;
.header, .header,
.headerRoll { .headerRoll {
@@ -1,28 +0,0 @@
import React, { memo } from 'react';
import { formatDisplay } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import styles from './Countdown.module.scss';
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
// prepare display string
const display =
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
const classes = `${small ? styles.countdownClockSmall : styles.countdownClock}
${isNegative ? styles.negative : ''}`;
return (
<div className={classes}>
{display}
</div>
);
};
export default memo(Countdown);
Countdown.propTypes = {
time: PropTypes.number,
small: PropTypes.bool,
isNegative: PropTypes.bool,
hideZeroHours: PropTypes.bool,
};
@@ -1,29 +0,0 @@
@use '../../../styles/main' as *;
/* Common */
.countdownClock,
.countdownClockSmall {
font-family: 'Open Sans', sans-serif;
line-height: 0.9;
color: $text-white;
}
/* Viewers */
.countdownClock {
font-size: 21vw;
line-height: 21vw;
text-align: center;
letter-spacing: 1vw;
}
/* Editor */
.countdownClockSmall {
font-size: 4em;
text-align: center;
letter-spacing: 0.1em;
font-weight: 600;
}
.negative {
color: $ontime-pink;
}
@@ -0,0 +1,26 @@
import { memo } from 'react';
import { formatDisplay } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import './TimerDisplay.scss';
const TimerDisplay = ({ time, small, isNegative, hideZeroHours }) => {
const display =
(time === null || typeof time === 'undefined' || isNaN(time))
? '-- : -- : --'
: formatDisplay(time, hideZeroHours);
const classes = `timer ${small ? 'timer--small' : ''} ${isNegative ? 'timer--finished' : ''}`;
return <div className={classes}>{display}</div>;
};
export default memo(TimerDisplay);
TimerDisplay.propTypes = {
time: PropTypes.number,
small: PropTypes.bool,
isNegative: PropTypes.bool,
hideZeroHours: PropTypes.bool,
};
@@ -0,0 +1,22 @@
@use '../../../theme/viewerDefs' as *;
.timer {
font-family: 'Open Sans', sans-serif;
color: var(--timer-color-override, $viewer-color);
font-size: 21vw;
line-height: 21vw;
text-align: center;
letter-spacing: 1vw;
&--small {
font-size: 4em;
line-height: 0.9;
text-align: center;
letter-spacing: 0.1em;
font-weight: 600;
}
&--finished {
color: $timer-finished-color;
}
}
@@ -1,8 +1,10 @@
/* eslint-disable react/destructuring-assignment */ /* eslint-disable react/destructuring-assignment */
import React from 'react'; import React from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { version as appVersion } from '../../../../package.json';
import { LoggingContext } from '../../context/LoggingContext';
import style from './ErrorBoundary.module.scss'; import style from './ErrorBoundary.module.scss';
const appVersion = require('../../../../package.json').version;
class ErrorBoundary extends React.Component { class ErrorBoundary extends React.Component {
static contextType = LoggingContext; static contextType = LoggingContext;
@@ -25,8 +27,8 @@ class ErrorBoundary extends React.Component {
}); });
try { try {
this.context.emitError(error.toString()); this.context.emitError(error.toString());
} catch { } catch (e) {
console.log('Unable to emit error') console.log('Unable to emit error', error, e )
} }
this.reportContent = `${error} ${info.componentStack}`; this.reportContent = `${error} ${info.componentStack}`;
} }
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *; @use '../../../theme/main' as *;
.errorContainer { .errorContainer {
width: 100%; width: 100%;
@@ -1,9 +1,10 @@
import React, { useCallback, useContext } from 'react'; import { useCallback, useContext } from 'react';
import EditableTimer from 'common/input/EditableTimer'; import EditableTimer from 'common/components/input/EditableTimer';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { LoggingContext } from '../../context/LoggingContext';
import { validateTimes } from '../../utils/entryValidator';
export default function EventTimes(props) { export default function EventTimes(props) {
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props; const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
const { emitWarning } = useContext(LoggingContext); const { emitWarning } = useContext(LoggingContext);
@@ -1,9 +1,11 @@
import React, { useCallback, useContext } from 'react'; import { useCallback, useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TimesDelayed from './TimesDelayed';
import { LoggingContext } from '../../context/LoggingContext';
import { validateTimes } from '../../utils/entryValidator';
import Times from './Times'; import Times from './Times';
import TimesDelayed from './TimesDelayed';
export default function EventTimesVertical(props) { export default function EventTimesVertical(props) {
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props; const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
@@ -1,7 +1,8 @@
import React from 'react';
import EditableTimer from '../../input/EditableTimer';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import style from './Times.module.scss'
import EditableTimer from '../input/EditableTimer';
import style from './Times.module.scss';
export default function Times(props) { export default function Times(props) {
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props; const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
@@ -37,7 +38,7 @@ export default function Times(props) {
/> />
</> </>
); );
}; }
Times.propTypes = { Times.propTypes = {
handleValidate: PropTypes.func.isRequired, handleValidate: PropTypes.func.isRequired,
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *; @use '../../../theme/main' as *;
.label { .label {
font-size: 0.75em; font-size: 0.75em;
@@ -1,8 +1,9 @@
import React from 'react';
import EditableTimer from '../../input/EditableTimer';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { stringFromMillis } from '../../utils/time'; import { stringFromMillis } from '../../utils/time';
import style from './Times.module.scss' import EditableTimer from '../input/EditableTimer';
import style from './Times.module.scss';
export default function TimesDelayed(props) { export default function TimesDelayed(props) {
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props; const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
@@ -45,7 +46,7 @@ export default function TimesDelayed(props) {
/> />
</> </>
); );
}; }
TimesDelayed.propTypes = { TimesDelayed.propTypes = {
handleValidate: PropTypes.func.isRequired, handleValidate: PropTypes.func.isRequired,
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { Textarea } from '@chakra-ui/react'; import { Textarea } from '@chakra-ui/react';
import autosize from 'autosize/dist/autosize'; import autosize from 'autosize/dist/autosize';
@@ -1,9 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react'; import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
import { clamp } from '../../app/utils/math';
import style from './TimeInput.module.css';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { clamp } from '../../utils/math';
import style from './TimeInput.module.css';
const inputProps = { const inputProps = {
width: 20, width: 20,
fontWeight: 400, fontWeight: 400,
@@ -1,8 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import style from './EditableText.module.scss';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import style from './EditableText.module.scss';
export default function EditableText(props) { export default function EditableText(props) {
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props; const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
const [text, setText] = useState(defaultValue || ''); const [text, setText] = useState(defaultValue || '');
@@ -1,4 +1,4 @@
@use '../../styles/main' as *; @use '../../../theme/main' as *;
.block { .block {
overflow: hidden; overflow: hidden;
@@ -1,9 +1,11 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { forgivingStringToMillis } from '../utils/dateConfig';
import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { stringFromMillis } from '../utils/time';
import { LoggingContext } from '../../context/LoggingContext';
import { forgivingStringToMillis } from '../../utils/dateConfig';
import { stringFromMillis } from '../../utils/time';
import style from './EditableTimer.module.scss'; import style from './EditableTimer.module.scss';
export default function EditableTimer(props) { export default function EditableTimer(props) {
@@ -1,4 +1,4 @@
@use '../../styles/main'as *; @use '../../../theme/main'as *;
.editable, .editable,
.delayedEditable { .delayedEditable {
@@ -1,6 +1,6 @@
import React from 'react'; import { clamp } from 'common/utils/math';
import { clamp } from 'app/utils/math';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styles from './MyProgressBar.module.scss'; import styles from './MyProgressBar.module.scss';
export default function MyProgressBar(props) { export default function MyProgressBar(props) {
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *; @use '../../../theme/main' as *;
.progress, .progress,
.progressCountdown { .progressCountdown {
+21 -39
View File
@@ -1,12 +1,18 @@
import React, { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion';
import navlogo from 'assets/images/logos/LOGO-72.png';
import style from './NavLogo.module.scss';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { Image } from '@chakra-ui/react';
import { IoContract } from '@react-icons/all-files/io5/IoContract';
import { IoExpand } from '@react-icons/all-files/io5/IoExpand'; import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
import navlogo from 'assets/images/logos/LOGO-72.png';
import { AnimatePresence, motion } from 'framer-motion';
import PropTypes from 'prop-types';
import useFullscreen from '../../hooks/useFullscreen';
import navigatorConstants from './navigatorConstants';
import style from './NavLogo.module.scss';
/* Styling for action buttons */ /* Styling for action buttons */
const navButtonStyle = { const navButtonStyle = {
@@ -19,6 +25,7 @@ const navButtonStyle = {
export default function NavLogo(props) { export default function NavLogo(props) {
const { isHidden } = props; const { isHidden } = props;
const [showNav, setShowNav] = useState(false); const [showNav, setShowNav] = useState(false);
const { isFullScreen, toggleFullScreen } = useFullscreen();
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setShowNav((prev) => !prev); setShowNav((prev) => !prev);
@@ -34,16 +41,6 @@ export default function NavLogo(props) {
} }
}, []); }, []);
const toggleFullscreen = useCallback(() => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}, []);
useEffect(() => { useEffect(() => {
// attach the event listener // attach the event listener
document.addEventListener('keydown', handleKeyPress); document.addEventListener('keydown', handleKeyPress);
@@ -65,6 +62,7 @@ export default function NavLogo(props) {
initial={{ opacity: showNav ? 0.5 : baseOpacity }} initial={{ opacity: showNav ? 0.5 : baseOpacity }}
whileHover={{ opacity: 1 }} whileHover={{ opacity: 1 }}
className={style.navContainer} className={style.navContainer}
data-testid='nav-logo'
> >
<Image alt='' src={navlogo} className={style.logo} onClick={handleClick} /> <Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
<AnimatePresence> <AnimatePresence>
@@ -79,8 +77,8 @@ export default function NavLogo(props) {
> >
<IconButton <IconButton
aria-label='Toggle Fullscreen' aria-label='Toggle Fullscreen'
icon={<IoExpand />} icon={isFullScreen ? <IoContract /> : <IoExpand />}
onClick={toggleFullscreen} onClick={toggleFullScreen}
{...navButtonStyle} {...navButtonStyle}
/> />
</motion.div> </motion.div>
@@ -91,27 +89,11 @@ export default function NavLogo(props) {
exit={{ opacity: 0, scaleY: 0, y: -50 }} exit={{ opacity: 0, scaleY: 0, y: -50 }}
className={style.nav} className={style.nav}
> >
<Link to='/timer' {...tabProps}> {navigatorConstants.map((route) => (
Timer <Link to={route.url} key={route.url} {...tabProps}>
</Link> {route.label}
<Link to='/minimal' {...tabProps}> </Link>
Minimal Timer ))}
</Link>
<Link to='/sm' {...tabProps}>
Backstage
</Link>
<Link to='/public' {...tabProps}>
Public
</Link>
<Link to='/lower' {...tabProps}>
Lower Thirds
</Link>
<Link to='/pip' {...tabProps}>
PIP
</Link>
<Link to='/studio' {...tabProps}>
Studio Clock
</Link>
</motion.div> </motion.div>
</> </>
)} )}
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *; @use '../../../theme/main' as *;
.navContainer { .navContainer {
position: absolute; position: absolute;
@@ -0,0 +1,13 @@
const navigatorConstants = [
{ url: '/timer', label: 'Timer' },
{ url: '/clock', label: 'Clock' },
{ url: '/minimal', label: 'Minimal Timer' },
{ url: '/backstage', label: 'Backstage' },
{ url: '/public', label: 'Public' },
{ url: '/lower', label: 'Lower Thirds' },
{ url: '/pip', label: 'PiP' },
{ url: '/studio', label: 'Studio Clock' },
{ url: '/countdown', label: 'Countdown' },
];
export default navigatorConstants;
@@ -1,9 +1,12 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import TodayItem from './TodayItem'; import { useInterval } from 'common/hooks/useInterval';
import { useInterval } from 'app/hooks/useInterval';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Empty from '../../state/Empty';
import style from './Paginator.module.scss'; import Empty from '../state/Empty';
import TodayItem from './TodayItem';
import './Paginator.scss';
export default function Paginator(props) { export default function Paginator(props) {
const { const {
@@ -67,7 +70,7 @@ export default function Paginator(props) {
} }
return ( return (
<div className={style.entries}> <div className='paginator entries'>
{page.map((e) => { {page.map((e) => {
if (e.id === selectedId) selectedState = 1; if (e.id === selectedId) selectedState = 1;
else if (selectedState === 1) selectedState = 2; else if (selectedState === 1) selectedState = 2;
@@ -0,0 +1,62 @@
@use '../../../theme/viewerDefs' as *;
.paginator.entries {
width: 100%;
.entry-times {
font-family: 'Open Sans', sans-serif;
font-size: 1.4vw;
min-width: 10vw;
opacity: 0.9;
letter-spacing: 0.035em;
}
.entry-title {
align-self: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.entry {
display: flex;
font-size: 1.3vw;
padding: 0.2vh 1vw;
margin-bottom: 1.5vh;
border-radius: 3px;
&--now,
&--future {
color: var(--color-override, $viewer-color);
}
&--past {
font-size: 1.2vw;
opacity: 0.6;
}
&--now {
line-height: 5vh;
background-color: $now-bg-color;
border-bottom: 0.5vh solid;
border-bottom-color: var(--accent-color-override, $accent-color);
margin-left: -0.5em;
padding-left: 0.5em;
}
&--future {
background-color: $future-bg-color;
}
}
.backstage-indicator {
color: var(--accent-color-override, $accent-color);
margin-left: auto;
margin-right: -0.5vw;
&::before {
content: '*';
}
}
}
@@ -0,0 +1,39 @@
import PropTypes from 'prop-types';
import { formatTime } from '../../utils/time';
import './Paginator.scss';
export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
// Format timers
const start = formatTime(timeStart, { format: 'hh:mm' });
const end = formatTime(timeEnd, { format: 'hh:mm' });
// user colours
const userColour = colour !== '' ? colour : 'transparent';
// select styling
let selectStyle = 'entry--past';
if (selected === 1) selectStyle = 'entry--now';
else if (selected === 2) selectStyle = 'entry--future';
return (
<div className={`entry ${selectStyle}`} style={{ borderLeft: `4px solid ${userColour}` }}>
<div className='entry-times'>
{`${start} · ${end}`}
</div>
<div className='entry-title'>{title}</div>
{backstageEvent && <div className='backstage-indicator' />}
</div>
);
}
TodayItem.propTypes = {
selected: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
title: PropTypes.string,
backstageEvent: PropTypes.bool,
colour: PropTypes.string,
};
@@ -1,9 +1,11 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck'; import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { AppContext } from '../../../app/context/AppContext'; import PropTypes from 'prop-types';
import { AppContext } from '../../context/AppContext';
import style from './ProtectRoute.module.scss'; import style from './ProtectRoute.module.scss';
export default function ProtectRoute({ children }) { export default function ProtectRoute({ children }) {
@@ -1,5 +1,5 @@
@use '../../../styles/main' as *; @use '../../../theme/main' as *;
@use '../../../styles/mixins' as *; @use '../../../theme/mixins' as *;
.container { .container {
background: $bg-black; background: $bg-black;
@@ -1,12 +1,12 @@
import React from 'react';
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg'; import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import style from './Empty.module.scss'; import style from './Empty.module.scss';
export default function Empty(props) { export default function Empty(props) {
const { text, ...rest } = props; const { text, dark, ...rest } = props;
return ( return (
<div className={style.emptyContainer} {...rest}> <div className={`${style.emptyContainer} ${dark ? style.dark : ''}`} {...rest}>
<Emptyimage className={style.empty} /> <Emptyimage className={style.empty} />
<span className={style.text}>{text}</span> <span className={style.text}>{text}</span>
</div> </div>
@@ -15,4 +15,5 @@ export default function Empty(props) {
Empty.propTypes = { Empty.propTypes = {
text: PropTypes.string, text: PropTypes.string,
dark: PropTypes.bool,
} }
@@ -0,0 +1,27 @@
@use '../../../theme/main' as *;
.emptyContainer {
width: 100%;
height: 100%;
text-align: center;
color: $bg-black-300;
.empty {
width: 100%;
opacity: 0.3;
}
.text {
font-weight: 600;
font-size: 2em;
}
&.dark {
background: $bg-black;
color: $bg-gray-500;
.empty {
opacity: 1;
}
}
}
@@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import style from './TitleCard.module.scss'; import style from './TitleCard.module.scss';
export default function TitleCard(props) { export default function TitleCard(props) {
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *; @use '../../../theme/main' as *;
.label { .label {
@include card-label; @include card-label;
@@ -0,0 +1,26 @@
import PropTypes from 'prop-types';
import './TitleSide.scss';
export default function TitleSide(props) {
const { type, label, title, subtitle, presenter } = props;
const isNext = type === 'next';
return (
<div className='title-side'>
<div className='label'>{label}</div>
<div className={isNext ? 'title next' : 'title now'}>{title}</div>
<div className={isNext ? 'presenter next' : 'presenter now'}>{presenter}</div>
<div className={isNext ? 'subtitle next' : 'subtitle now'}>{subtitle}</div>
</div>
);
}
TitleSide.propTypes = {
type: PropTypes.oneOf(['now', 'next']),
label: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
presenter: PropTypes.string,
}
@@ -0,0 +1,61 @@
@use '../../../theme/viewerDefs' as *;
.title-side {
.label {
font-size: 1.3vw;
color: var(--accent-color-override, $accent-color);
}
.title,
.subtitle,
.presenter {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title {
color: $title-color;
font-weight: 600;
&.now {
font-size: 2.5vw;
}
&.next {
font-size: 2vw;
}
}
.subtitle {
color: $subtitle-color;
&.now {
font-size: 1.2vw;
margin-top: -1vh;
}
&.next {
font-size: 1.1vw;
margin-top: -0.8vh;
}
}
.presenter {
font-weight: 400;
color: $subtitle-color;
&.now {
font-size: 2vw;
}
&.next {
font-size: 1.5vw;
}
}
.title:after,
.subtitle:after,
.presenter:after {
content: '\200b';
}
}
@@ -0,0 +1,45 @@
@use '../../../theme/main' as *;
.modalBody {
min-height: 40vh;
display: flex;
flex-direction: column;
gap: 16px;
.options {
margin-bottom: 1.5em;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.notes {
font-size: 0.9em;
}
.info {
background-color: $bg-gray;
margin: 1em 0;
padding: 0.5em;
border-radius: 2px;
color: black;
position: relative;
}
.corner {
position: absolute;
right: 4px;
top: 4px;
}
.infoList {
font-size: 0.9em;
padding-left: 8px;
}
.flexColumnLeft {
display: flex;
flex-direction: column;
align-items: flex-start;
}
}
@@ -0,0 +1,137 @@
import { ChangeEvent, useCallback, useContext, useRef, useState } from 'react';
import { Button } from '@chakra-ui/button';
import {
Checkbox,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Progress,
} from '@chakra-ui/react';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import { useQueryClient } from '@tanstack/react-query';
import { EVENTS_TABLE } from '../../api/apiConstants';
import { uploadEvents } from '../../api/ontimeApi';
import { LoggingContext } from '../../context/LoggingContext';
import TooltipActionBtn from '../buttons/TooltipActionBtn';
import { validateFile } from './utils';
import style from './UploadModal.module.scss';
interface UploadModalProps {
onClose: () => void;
isOpen: boolean;
}
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext);
const [errors, setErrors] = useState<string[]>([]);
const [file, setFile] = useState<File | null>(null);
const [progress, setProgress] = useState(0);
const overrideOptionRef = useRef<HTMLInputElement>(null);
const handleFile = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const fileUploaded = event?.target?.files?.[0];
if (!fileUploaded) return;
const validate = validateFile(fileUploaded);
setErrors(validate.errors);
if (validate.isValid) {
setFile(fileUploaded);
} else {
setFile(null);
}
}, []);
const handleUpload = useCallback(async () => {
if (file) {
try {
await uploadEvents(file, setProgress, { onlyEvents: overrideOptionRef?.current?.checked });
} catch (error) {
emitError(`Failed uploading file: ${error}`);
} finally {
await queryClient.invalidateQueries(EVENTS_TABLE);
setFile(null);
}
}
}, [emitError, file, queryClient]);
return (
<Modal
onClose={onClose}
isOpen={isOpen}
closeOnOverlayClick={false}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>File upload</ModalHeader>
<ModalCloseButton />
<ModalBody className={style.modalBody}>
<FormControl isInvalid={errors.length > 0}>
<FormLabel>Select file to upload</FormLabel>
<Input type='file' onChange={handleFile} accept='.json, .xlsx' />
{errors.length === 0 ? (
<FormHelperText>.XLSX .JSON with max 1MB</FormHelperText>
) : (
<FormErrorMessage className={style.flexColumnLeft}>
{errors.map((error) => (
<span key={error}>{error}</span>
))}
</FormErrorMessage>
)}
</FormControl>
<div className={style.options}>
<b>Options</b>
<Checkbox ref={overrideOptionRef}>Import only events</Checkbox>
<span className={style.notes}>This will prevent overriding user settings</span>
</div>
{file && (
<div className={style.info}>
<span>File ready to upload</span>
<TooltipActionBtn
clickHandler={() => setFile(null)}
tooltip='Cancel'
aria-label='Cancel'
className={style.corner}
size='sm'
variant='ghosted'
icon={<IoCloseSharp />}
/>
<ul className={style.infoList}>
<li>{file.name}</li>
<li>{`${(file.size / 1024).toFixed(2)}kb`}</li>
<li>{file.type}</li>
</ul>
</div>
)}
<Progress value={progress} />
</ModalBody>
<ModalFooter>
<Button
colorScheme='blue'
disabled={!file || errors.length > 0}
onClick={handleUpload}
isLoading={progress < 0 && progress >= 100}
>
Upload
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,25 @@
type ValidationStatus = {
errors: string[];
isValid: boolean;
};
export function validateFile(file: File): ValidationStatus {
const status:ValidationStatus = { errors: [], isValid: true };
if (!file) {
status.errors.push('No file to upload');
status.isValid = false;
}
// Limit file size to 1MB
if (file.size > 1000000) {
status.errors.push('File size limit (1MB) exceeded');
status.isValid = false;
}
// Check file extension
if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.json')) {
status.errors.push('Unhandled file type');
status.isValid = false;
}
return status;
}
@@ -1,64 +0,0 @@
@use '../../../styles/main' as *;
.entries {
width: 100%;
}
.entryTimes {
font-family: 'Open Sans', sans-serif;
font-size: 1.4vw;
min-width: 10vw;
opacity: 0.9;
letter-spacing: 0.035em;
}
.entryTitle {
align-self: center;
@include ellipsis;
}
.entryPast,
.entryNow,
.entryFuture {
display: flex;
font-size: 1.3vw;
padding: 0.2vh 1vw;
margin-bottom: 1.5vh;
border-radius: 3px;
}
.entryPast {
font-size: 1.2vw;
}
.entryPast > .entryTitle {
color: $text-gray-disabled;
}
.entryNow,
.entryFuture {
color: $text-white;
}
.entryFuture {
background-color: $bg-gray-950;
}
.entryNow {
line-height: 5vh;
background-color: $bg-gray-900;
border-bottom: 0.5vh solid $ontime-pink;
margin-left: -0.5em;
padding-left: 0.5em;
}
.backstageInd {
color: $ontime-pink;
margin-left: auto;
margin-right: -0.5vw;
}
.backstageInd::before {
content: '*';
}
@@ -1,28 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './TitleSide.module.scss';
export default function TitleSide(props) {
const { type, label, title, subtitle, presenter } = props;
const titleStyle = type === 'next' ? style.nextTitle : style.nowTitle;
const subStyle = type === 'next' ? style.nextSubtitle : style.nowSubtitle;
const presStyle = type === 'next' ? style.nextPresenter : style.nowPresenter;
return (
<>
<div className={style.label}>{label}</div>
<div className={titleStyle}>{title}</div>
<div className={presStyle}>{presenter}</div>
<div className={subStyle}>{subtitle}</div>
</>
);
}
TitleSide.propTypes = {
type: PropTypes.oneOf(['now', 'next']),
label: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
presenter: PropTypes.string,
}
@@ -1,54 +0,0 @@
@use '../../../styles/main' as *;
.label {
@include card-label;
}
.nowTitle,
.nextTitle {
@include card-title;
}
.nowSubtitle,
.nowPresenter,
.nextSubtitle,
.nextPresenter {
color: $subtitle-gray;
}
.nowTitle {
font-size: 2.5vw;
}
.nowPresenter {
font-size: 2vw;
font-weight: 400;
}
.nowSubtitle {
font-size: 1.2vw;
margin-top: -1vh;
}
.nextTitle {
font-size: 2vw;
}
.nextSubtitle {
margin-top: -0.8vh;
font-size: 1.1vw;
}
.nextPresenter {
font-weight: 400;
font-size: 1.5vw;
}
.nowTitle:after,
.nowSubtitle:after,
.nowPresenter:after,
.nextTitle:after,
.nextSubtitle:after,
.nextPresenter:after {
content: '\200b';
}
@@ -1,38 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { stringFromMillis } from '../../utils/time';
import style from './Paginator.module.scss';
export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
// Format timers
const start = stringFromMillis(timeStart, false) || '';
const end = stringFromMillis(timeEnd, false) || '';
// user colours
const userColour = colour !== '' ? colour : 'transparent';
// select styling
let selectStyle = style.entryPast;
if (selected === 1) selectStyle = style.entryNow;
else if (selected === 2) selectStyle = style.entryFuture;
return (
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : ''}`}>
{`${start} · ${end}`}
</div>
<div className={style.entryTitle}>{title}</div>
{backstageEvent && <div className={style.backstageInd} />}
</div>
);
}
TodayItem.propTypes = {
selected: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
title: PropTypes.string,
backstageEvent: PropTypes.bool,
colour: PropTypes.string,
};
@@ -1,7 +1,8 @@
import React, { createContext, useCallback, useEffect, useState } from 'react'; import { createContext, useCallback, useEffect, useState } from 'react';
import { useFetch } from '../hooks/useFetch';
import { APP_SETTINGS } from '../api/apiConstants'; import { APP_SETTINGS } from '../api/apiConstants';
import { getSettings } from '../api/ontimeApi'; import { getSettings } from '../api/ontimeApi';
import { useFetch } from '../hooks/useFetch';
export const AppContext = createContext({ export const AppContext = createContext({
auth: false, auth: false,
@@ -1,4 +1,5 @@
import React, { createContext, useCallback } from 'react'; import { createContext, useCallback } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage'; import { useLocalStorage } from '../hooks/useLocalStorage';
export const CollapseContext = createContext({ export const CollapseContext = createContext({
@@ -1,4 +1,5 @@
import React, { createContext, useCallback, useMemo, useState } from 'react'; import { createContext, useCallback, useMemo, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage'; import { useLocalStorage } from '../hooks/useLocalStorage';
export const CursorContext = createContext({ export const CursorContext = createContext({
@@ -1,20 +1,47 @@
import React, { createContext, useCallback, useEffect, useState } from 'react'; import { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
import { useSocket } from './socketContext';
import { generateId } from '../../common/utils/generate_id';
import { nowInMillis, stringFromMillis } from '../../common/utils/time';
export const LoggingContext = createContext({ import { generateId } from '../utils/generate_id';
import { nowInMillis, stringFromMillis } from '../utils/time';
import { useSocket } from './socketContext';
type LOG_LEVEL = 'INFO' | 'WARN' | 'ERROR';
type Log = {
id: string;
origin: string;
time: string;
level: LOG_LEVEL;
text: string;
};
interface LoggingProviderState {
logData: Log[];
emitInfo: (text: string) => void;
emitWarning: (text: string) => void;
emitError: (text: string) => void;
clearLog: () => void;
}
type LoggingProviderProps = {
children: ReactNode
}
const notInitialised = () => {
throw new Error("Not initialised");
};
export const LoggingContext = createContext<LoggingProviderState>({
logData: [], logData: [],
emitInfo: () => undefined, emitInfo: notInitialised,
emitWarning: () => undefined, emitWarning: notInitialised,
emitError: () => undefined, emitError: notInitialised,
clearLog: () => undefined, clearLog: notInitialised
}); });
export const LoggingProvider = ({ children }) => { export const LoggingProvider = ({ children }: LoggingProviderProps) => {
const MAX_MESSAGES = 100; const MAX_MESSAGES = 100;
const socket = useSocket(); const socket = useSocket();
const [logData, setLogData] = useState([]); const [logData, setLogData] = useState<Log[]>([]);
const origin = 'USER'; const origin = 'USER';
// handle incoming messages // handle incoming messages
@@ -24,7 +51,7 @@ export const LoggingProvider = ({ children }) => {
// Ask for log data // Ask for log data
socket.emit('get-logger'); socket.emit('get-logger');
socket.on('logger', (data) => { socket.on('logger', (data: Log) => {
setLogData((l) => [data, ...l]); setLogData((l) => [data, ...l]);
}); });
@@ -41,9 +68,9 @@ export const LoggingProvider = ({ children }) => {
* @private * @private
*/ */
const _send = useCallback( const _send = useCallback(
(text, level) => { (text: string, level: LOG_LEVEL) => {
if (socket != null) { if (socket != null) {
const m = { const m: Log = {
id: generateId(), id: generateId(),
origin, origin,
time: stringFromMillis(nowInMillis()), time: stringFromMillis(nowInMillis()),
@@ -54,7 +81,7 @@ export const LoggingProvider = ({ children }) => {
socket.emit('logger', m); socket.emit('logger', m);
} }
if (logData.length > MAX_MESSAGES) { if (logData.length > MAX_MESSAGES) {
setLogData((l) => l.pop()); setLogData((l) => l.slice(1));
} }
}, },
[logData, socket] [logData, socket]
@@ -65,7 +92,7 @@ export const LoggingProvider = ({ children }) => {
* @param text * @param text
*/ */
const emitInfo = useCallback( const emitInfo = useCallback(
(text) => { (text: string) => {
_send(text, 'INFO'); _send(text, 'INFO');
}, },
[_send] [_send]
@@ -76,7 +103,7 @@ export const LoggingProvider = ({ children }) => {
* @param text * @param text
*/ */
const emitWarning = useCallback( const emitWarning = useCallback(
(text) => { (text: string) => {
_send(text, 'WARN'); _send(text, 'WARN');
}, },
[_send] [_send]
@@ -87,7 +114,7 @@ export const LoggingProvider = ({ children }) => {
* @param text * @param text
*/ */
const emitError = useCallback( const emitError = useCallback(
(text) => { (text: string) => {
_send(text, 'ERROR'); _send(text, 'ERROR');
}, },
[_send] [_send]
@@ -1,4 +1,5 @@
import React, { createContext, useCallback, useState } from 'react'; import { createContext, useCallback, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage'; import { useLocalStorage } from '../hooks/useLocalStorage';
export const TableSettingsContext = createContext({ export const TableSettingsContext = createContext({
@@ -0,0 +1,44 @@
// @ts-nocheck
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { serverURL } from 'common/api/apiConstants';
import io, { Socket } from 'socket.io-client';
interface SocketProviderState {
socket: Socket | null;
emit: <T>(topic: string, payload?: T) => void;
on: <T>(topic: string, callback: (data: T) => void) => void;
off: (topic: string) => void;
}
type SocketProviderProps = {
children: ReactNode;
};
const SocketContext = createContext<SocketProviderState>({
socket: null,
emit: () => {},
on: () => {},
off: () => {}
});
export const useSocket = () => {
return useContext(SocketContext);
};
function SocketProvider({ children }: SocketProviderProps) {
const [socket, setSocket] = useState(null);
useEffect(() => {
const socketInstance = io(serverURL, { transports: ["websocket"] });
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
};
}, []);
return (
<SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
);
}
export default SocketProvider;
@@ -0,0 +1,12 @@
// @ts-nocheck
export default function useElectronEvent() {
const isElectron = window?.process?.type === 'renderer';
const sendToElectron = (channel: string, args: any) => {
if (isElectron) {
window?.ipcRenderer.send(channel, args);
}
};
return { isElectron, sendToElectron };
}
@@ -1,4 +1,5 @@
import { useQuery } from 'react-query'; import { useQuery } from '@tanstack/react-query';
const refetchIntervalMs = 10000; const refetchIntervalMs = 10000;
/** /**
@@ -10,7 +11,6 @@ export const useFetch = (namespace, fn) => {
const { data, status, isError, refetch } = useQuery(namespace, fn, { const { data, status, isError, refetch } = useQuery(namespace, fn, {
refetchInterval: refetchIntervalMs, refetchInterval: refetchIntervalMs,
cacheTime: Infinity, cacheTime: Infinity,
notifyOnChangeProps: 'tracked',
}); });
return { data, status, isError, refetch }; return { data, status, isError, refetch };
+177
View File
@@ -0,0 +1,177 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
export type TLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
export type TOptions = {
logLevel?: TLogLevel;
maxFontSize?: number;
minFontSize?: number;
onFinish?: (fontSize: number) => void;
onStart?: () => void;
resolution?: number;
};
const LOG_LEVEL: Record<TLogLevel, number> = {
debug: 10,
info: 20,
warn: 30,
error: 40,
none: 100,
};
const useFitText = ({
logLevel: logLevelOption = 'info',
maxFontSize = 100,
minFontSize = 20,
onFinish,
onStart,
resolution = 5,
}: TOptions = {}) => {
const logLevel = LOG_LEVEL[logLevelOption];
const initState = useCallback(() => {
return {
calcKey: 0,
fontSize: maxFontSize,
fontSizePrev: minFontSize,
fontSizeMax: maxFontSize,
fontSizeMin: minFontSize,
};
}, [maxFontSize, minFontSize]);
const ref = useRef<HTMLDivElement>(null);
const innerHtmlPrevRef = useRef<string | null>();
const isCalculatingRef = useRef(false);
const [state, setState] = useState(initState);
const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;
// Monitor div size changes and recalculate on resize
let animationFrameId: number | null = null;
const [ro] = useState(
() =>
new ResizeObserver(() => {
animationFrameId = window.requestAnimationFrame(() => {
if (isCalculatingRef.current) {
return;
}
onStart && onStart();
isCalculatingRef.current = true;
// `calcKey` is used in the dependencies array of
// `useIsoLayoutEffect` below. It is incremented so that the font size
// will be recalculated even if the previous state didn't change (e.g.
// when the text fit initially).
setState({
...initState(),
calcKey: calcKey + 1,
});
});
}),
);
useEffect(() => {
if (ref.current) {
ro.observe(ref.current);
}
return () => {
animationFrameId && window.cancelAnimationFrame(animationFrameId);
ro.disconnect();
};
}, [animationFrameId, ro]);
// Recalculate when the div contents change
const innerHtml = ref.current && ref.current.innerHTML;
useEffect(() => {
if (calcKey === 0 || isCalculatingRef.current) {
return;
}
if (innerHtml !== innerHtmlPrevRef.current) {
onStart && onStart();
setState({
...initState(),
calcKey: calcKey + 1,
});
}
innerHtmlPrevRef.current = innerHtml;
}, [calcKey, initState, innerHtml, onStart]);
// Check overflow and resize font
useLayoutEffect(() => {
// Don't start calculating font size until the `resizeKey` is incremented
// above in the `ResizeObserver` callback. This avoids an extra resize
// on initialization.
if (calcKey === 0) {
return;
}
const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;
const isOverflow =
!!ref.current &&
(ref.current.scrollHeight > ref.current.offsetHeight ||
ref.current.scrollWidth > ref.current.offsetWidth);
const isFailed = isOverflow && fontSize === fontSizePrev;
const isAsc = fontSize > fontSizePrev;
// Return if the font size has been adjusted "enough" (change within `resolution`)
// reduce font size by one increment if it's overflowing.
if (isWithinResolution) {
if (isFailed) {
isCalculatingRef.current = false;
if (logLevel <= LOG_LEVEL.info) {
console.info(
`[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`,
);
}
} else if (isOverflow) {
setState({
fontSize: isAsc ? fontSizePrev : fontSizeMin,
fontSizeMax,
fontSizeMin,
fontSizePrev,
calcKey,
});
} else {
isCalculatingRef.current = false;
onFinish && onFinish(fontSize);
}
return;
}
// Binary search to adjust font size
let delta: number;
let newMax = fontSizeMax;
let newMin = fontSizeMin;
if (isOverflow) {
delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;
newMax = Math.min(fontSizeMax, fontSize);
} else {
delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;
newMin = Math.max(fontSizeMin, fontSize);
}
setState({
calcKey,
fontSize: fontSize + delta / 2,
fontSizeMax: newMax,
fontSizeMin: newMin,
fontSizePrev: fontSize,
});
}, [
calcKey,
fontSize,
fontSizeMax,
fontSizeMin,
fontSizePrev,
onFinish,
ref,
resolution,
]);
return { fontSize: `${fontSize}%`, ref };
};
export default useFitText;
+31
View File
@@ -0,0 +1,31 @@
import { useCallback, useEffect, useState } from 'react';
export default function useFullscreen() {
const [isFullScreen, setFullScreen] = useState(document.fullscreenElement);
useEffect(() => {
const handleChange = () => {
setFullScreen(document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleChange, { passive: true });
document.addEventListener('resize', handleChange, { passive: true });
return () => {
document.removeEventListener('fullscreenchange', handleChange, { passive: true });
document.removeEventListener('resize', handleChange, { passive: true });
}; }, []);
const toggleFullScreen = useCallback(() => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
setFullScreen(document.fullscreenElement);
}, []);
return { isFullScreen, toggleFullScreen };
}
@@ -1,4 +1,5 @@
import { useMutation, useQueryClient } from 'react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { EVENTS_TABLE } from '../api/apiConstants'; import { EVENTS_TABLE } from '../api/apiConstants';
/** /**
@@ -0,0 +1,46 @@
import { useEffect, useState } from 'react';
const scriptTagId = 'ontime-override';
export const useRuntimeStylesheet = (pathToFile) => {
const [shouldRender, setShouldRender] = useState(false);
useEffect(() => {
const fetchData = async () => {
let response = await fetch(pathToFile);
if (response.ok) {
return await response.text();
}
};
if (!pathToFile) {
document.getElementById(scriptTagId)?.remove();
setShouldRender(true);
return;
}
if (document.getElementById(scriptTagId)) {
setShouldRender(true);
return;
}
setShouldRender(false);
const styleSheet = document.createElement('style');
styleSheet.rel = 'stylesheet';
styleSheet.setAttribute('id', scriptTagId);
fetchData()
.then((data) => {
styleSheet.innerHTML = data;
document.head.append(styleSheet);
})
.catch((error) => {
console.error(`Error loading stylesheet: ${error}`);
})
.finally(() => {
// schedule render for next tick
setTimeout(() => setShouldRender(true), 0);
});
}, [pathToFile]);
return { shouldRender };
};

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