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:
build_mac:
runs-on: macOS-latest
timeout-minutes: 20
env:
CI: ''
@@ -22,7 +23,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install --frozen-lockfile
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
@@ -31,13 +32,13 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install --frozen-lockfile && yarn setdb
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-mac
@@ -53,6 +54,7 @@ jobs:
build_win:
runs-on: windows-latest
timeout-minutes: 20
env:
CI: ''
@@ -65,7 +67,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install --frozen-lockfile
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
@@ -74,13 +76,13 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install --frozen-lockfile && yarn setdb
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-win
@@ -96,6 +98,7 @@ jobs:
build_linux:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
@@ -108,7 +111,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install --frozen-lockfile
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
@@ -117,13 +120,13 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install && yarn setdb
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-linux
@@ -139,6 +142,7 @@ jobs:
publish_docker:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
@@ -153,7 +157,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install
run: yarn install --network-timeout 300000
working-directory: ./client
- name: React - Build project
@@ -162,7 +166,7 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# Login to docker
@@ -171,14 +175,16 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Prepare builder
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
# Build and push
- name: Build and push
working-directory: ./
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
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:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
@@ -50,4 +51,4 @@ jobs:
uses: cypress-io/github-action@v2
with:
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
TODO.md
ontime-db/
ontime-external/
# vscode stuff
.vscode/*
+1
View File
@@ -46,5 +46,6 @@
</list>
</option>
</inspection_tool>
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</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-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/win-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/></a>
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/dockerhub.png"/></a>
</div>
# Ontime
@@ -65,13 +66,14 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
- Public Info
- Picture in Picture
- Studio Clock
- [Make your own?](#make-your-own-viewer)
- [Make your own?](#make-your-own-viewer)
- [x] Configurable realtime Lower Thirds
- [x] Cuesheets with additional custom fields
- [x] Send live messages to different screen types
- [x] Ability to differentiate between backstage and public data
- [x] Manage delays workflow
- [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] Import event list from Excel
- [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)
- [x] Multi platform (available on Windows, MacOS and Linux)
- [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
@@ -106,7 +110,9 @@ Taking advantage of the integrations in Ontime, we currently use Ontime with:
webmodule
- `OBS`: **render views** using the Browser Module
- `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
@@ -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)
### Headless run
You can self host and run ontime in a docker image, the run command should:
- expose the necessary ports (listen in Dockerfile)
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
- 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
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.
- [ ] HTTP Server (vMix integration)
- [ ] Improvement with event component design
- [ ] New playback mode
for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
- [ ] Companion module
- [ ] Improvements in event interface
- [ ] Moderator view
- [ ] New playback mode for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
- [ ] Lower Third Manager
- [ ] Note only event
- [ ] Reach Schedule: way to speedup timer to meet a deadline
### Issues
The app is still in pre-release and there are a few issues, mainly concerning responsiveness in
different screens. If you run into problems, please open an issue with a screenshot and note your
device and its screen resolution
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
#### Unsigned App
@@ -176,17 +182,19 @@ or in Linux
```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 macOS: after attempting to run the installer, navigate to System Preferences -> Security & Privacy and allow the execution of the app
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing File as a Program
- in macOS: after attempting to run the installer, navigate to System Preferences -> Security &
Privacy and allow the execution of the app
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
File as a Program
Long story short: Ontime app is unsigned. </br>Purchasing the certificates for both Mac and Windows
would mean a recurrent expense and is not a priority. This is unlikely to change in future. If you
have tips on how to improve this, or would like to sponsor the code signing,
please [open an issue, so we can discuss it](https://github.com/cpvalente/ontime/issues/new)
#### Safari
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": [
"react-app",
"react-app/jest",
"plugin:react/recommended"
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier"
],
"plugins": [
"react",
"testing-library",
"jest"
"simple-import-sort"
],
"rules": {
"jest/no-mocks-import": "warn",
"no-useless-concat": "warn",
"prefer-template": "warn",
"react/jsx-no-bind": [
@@ -41,6 +50,8 @@
"react/no-deprecated": [
"error"
],
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-no-undef": "error",
"react/jsx-closing-tag-location": "warn",
"react/jsx-no-useless-fragment": "warn",
@@ -77,6 +88,56 @@
"customValidators": [],
"skipUndeclared": true
}
]
}
],
"simple-import-sort/imports": "warn",
"simple-import-sort/exports": "warn"
},
"overrides": [
// override "simple-import-sort" config
{
"files": [
"*.js",
"*.jsx",
"*.ts",
"*.tsx"
],
"rules": {
"simple-import-sort/imports": [
"error",
{
"groups": [
// Packages `react` related packages come first.
[
"^react",
"^@?\\w"
],
// Internal packages.
[
"^(@|components)(/.*|$)"
],
// Side effect imports.
[
"^\\u0000"
],
// Parent imports. Put `..` last.
[
"^\\.\\.(?!/?$)",
"^\\.\\./?$"
],
// Other relative imports. Put same-folder imports and `.` last.
[
"^\\./(?=.*/)(?!/?$)",
"^\\.(?!/?$)",
"^\\./?$"
],
// Style imports.
[
"^.+\\.?(css)$"
]
]
}
]
}
}
]
}
+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",
"version": "1.1.1",
"version": "1.8.2",
"private": true,
"dependencies": {
"@chakra-ui/react": "^2.0.0-next.3",
"@dnd-kit/core": "^5.0.3",
"@dnd-kit/sortable": "^6.0.1",
"@dnd-kit/utilities": "^3.1.0",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@chakra-ui/react": "^2.3.2",
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@react-icons/all-files": "^4.1.0",
"@tanstack/react-query": "^4.1.3",
"@tanstack/react-query-devtools": "^4.0.10",
"autosize": "^5.0.1",
"axios": "^0.25.0",
"axios": "^0.27.2",
"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-beautiful-dnd": "^13.1.0",
"react-dom": "^18.1.0",
"react-fast-compare": "^3.2.0",
"react-qr-code": "^2.0.5",
"react-query": "^3.38.0",
"react-router-dom": "^6.3.0",
"react-table": "^7.7.0",
"socket.io-client": "^4.5.0",
"socket.io-client": "^4.5.1",
"typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "set BROWSER=none&&react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
"eject": "react-scripts eject"
"start": "set BROWSER=none&&vite",
"build": "vite build",
"lint": "eslint .",
"stylelint": "npx stylelint \"**/*.scss\"\n",
"test": "vitest ",
"test:pipeline": "vitest run"
},
"browserslist": {
"production": [
@@ -51,9 +54,30 @@
"@testing-library/react": "^13.1.1",
"@testing-library/react-hooks": "^8.0.0",
"@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",
"react-scripts": "^5.0.1",
"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 { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import './App.scss';
import withSocket from 'features/viewers/ViewWrapper';
import { Suspense, useCallback, useEffect } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
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'));
const Table = lazy(() => import('features/table/ProtectedTable'));
import { AppContextProvider } from './common/context/AppContext';
import SocketProvider from './common/context/socketContext';
import theme from './theme/theme';
import AppRouter from './AppRouter';
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer'));
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'));
// Load Open Sans typeface
import('typeface-open-sans');
export const ontimeQueryClient = new QueryClient();
function App() {
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
const navigate = useNavigate();
// Handle keyboard shortcuts
const handleKeyPress = useCallback((e) => {
@@ -64,86 +42,25 @@ function App() {
};
}, [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 (
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<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='/sm' element={<SStageManager />} />
<Route path='/backstage' element={<SStageManager />} />
<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>
</Suspense>
</ErrorBoundary>
</div>
<ChakraProvider resetCSS theme={theme}>
<SocketProvider>
<QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider>
<BrowserRouter>
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<AppRouter />
</Suspense>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
</BrowserRouter>
</AppContextProvider>
</QueryClientProvider>
</SocketProvider>
</ChakraProvider>
);
}
-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({
defaultOptions: {
@@ -6,4 +6,4 @@ export const queryClientMock = new QueryClient({
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
const minimalLocation = 'minimal';
const speakerLocation = 'speaker';
const smLocation = 'sm';
const publicLocation = 'public';
@@ -7,6 +8,7 @@ const studioLocation = 'studio';
const cuesheetLocation = 'cuesheet';
export const viewerLocations = [
{ link: minimalLocation, label: 'Minimal timer' },
{ link: speakerLocation, label: 'Speaker Screen' },
{ link: smLocation, label: 'Backstage 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 { eventURL } from './apiConstants';
/**
@@ -1,4 +1,5 @@
import axios from 'axios';
import { eventsURL } from './apiConstants';
/**
@@ -1,4 +1,5 @@
import axios from 'axios';
import { ontimeURL } from './apiConstants';
/**
@@ -18,7 +19,12 @@ export const ontimePlaceholderInfo = {
* @type {{pinCode: null}}
*/
export const ontimePlaceholderSettings = {
app: 'ontime',
version: 1,
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
};
/**
@@ -61,6 +67,14 @@ export const oscPlaceholderSettings = {
enabled: false,
};
/**
* @description placeholder information for view settings
* @type {{overrideCSS: boolean}}
*/
export const viewsPlaceholder = {
overrideStyles: false,
};
/**
* @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}}}
@@ -157,6 +171,21 @@ export const getInfo = async () => {
*/
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
* @return {Promise}
@@ -222,9 +251,7 @@ export const downloadEvents = async () => {
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
const url = window.URL.createObjectURL(
new Blob([response.data], { type: 'application/json' })
);
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
@@ -237,18 +264,26 @@ export const downloadEvents = async () => {
* @description HTTP request to upload events db
* @return {Promise}
*/
export const uploadEvents = async (file) => {
export const uploadEvents = async (file, setProgress, options) => {
const formData = new FormData();
formData.append('userFile', file); // appending file
await axios.post(`${ontimeURL}/db`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
formData.append('userFile', file);
const onlyEvents = options?.onlyEvents;
await axios
.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
* @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 { 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 { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function ActionButtons(props) {
const { showAdd, showDelay, showBlock, actionHandler } = props;
@@ -17,7 +18,7 @@ export default function ActionButtons(props) {
return (
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add ...' delay={500}>
<Tooltip label='Add ...' delay={tooltipDelayMid}>
<MenuButton
as={IconButton}
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 { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import PropTypes from 'prop-types';
export default function EnableBtn(props) {
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function PauseIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label='Pause timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPause size='24px' />}
colorScheme='orange'
@@ -1,4 +1,3 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
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 {
AlertDialog,
@@ -11,7 +11,8 @@ import {
import { Tooltip } from '@chakra-ui/tooltip';
import { FiPower } from '@react-icons/all-files/fi/FiPower';
import PropTypes from 'prop-types';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { LoggingContext } from '../../context/LoggingContext';
export default function QuitIconBtn(props) {
const { clickHandler, size = 'lg', ...rest } = props;
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function RollIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label='Roll mode' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoTimeOutline size='24px' />}
colorScheme='blue'
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function StartIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label='Start timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlay size='24px' />}
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 { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
@@ -1,12 +1,13 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function TransportIconBtn(props) {
const { clickHandler, icon, tooltip, disabled, ...rest } = props;
return (
<Tooltip label={tooltip} openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label={tooltip} openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={icon}
colorScheme='white'
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function UnloadIconBtn(props) {
const { clickHandler, disabled, ...rest } = props;
return (
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label='Unload event' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoStop size='22px' />}
colorScheme='red'
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Icon } from '@chakra-ui/react';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import PropTypes from 'prop-types';
import style from './CollapseBar.module.scss';
export default function CollapseBar(props) {
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.header,
.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 */
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';
const appVersion = require('../../../../package.json').version;
class ErrorBoundary extends React.Component {
static contextType = LoggingContext;
@@ -25,8 +27,8 @@ class ErrorBoundary extends React.Component {
});
try {
this.context.emitError(error.toString());
} catch {
console.log('Unable to emit error')
} catch (e) {
console.log('Unable to emit error', error, e )
}
this.reportContent = `${error} ${info.componentStack}`;
}
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.errorContainer {
width: 100%;
@@ -1,9 +1,10 @@
import React, { useCallback, useContext } from 'react';
import EditableTimer from 'common/input/EditableTimer';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import { useCallback, useContext } from 'react';
import EditableTimer from 'common/components/input/EditableTimer';
import PropTypes from 'prop-types';
import { LoggingContext } from '../../context/LoggingContext';
import { validateTimes } from '../../utils/entryValidator';
export default function EventTimes(props) {
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
const { emitWarning } = useContext(LoggingContext);
@@ -1,9 +1,11 @@
import React, { useCallback, useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import { useCallback, useContext } from 'react';
import PropTypes from 'prop-types';
import TimesDelayed from './TimesDelayed';
import { LoggingContext } from '../../context/LoggingContext';
import { validateTimes } from '../../utils/entryValidator';
import Times from './Times';
import TimesDelayed from './TimesDelayed';
export default function EventTimesVertical(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 style from './Times.module.scss'
import EditableTimer from '../input/EditableTimer';
import style from './Times.module.scss';
export default function Times(props) {
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
@@ -37,7 +38,7 @@ export default function Times(props) {
/>
</>
);
};
}
Times.propTypes = {
handleValidate: PropTypes.func.isRequired,
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.label {
font-size: 0.75em;
@@ -1,8 +1,9 @@
import React from 'react';
import EditableTimer from '../../input/EditableTimer';
import PropTypes from 'prop-types';
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) {
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
@@ -45,7 +46,7 @@ export default function TimesDelayed(props) {
/>
</>
);
};
}
TimesDelayed.propTypes = {
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 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 { clamp } from '../../app/utils/math';
import style from './TimeInput.module.css';
import PropTypes from 'prop-types';
import { clamp } from '../../utils/math';
import style from './TimeInput.module.css';
const inputProps = {
width: 20,
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 style from './EditableText.module.scss';
import PropTypes from 'prop-types';
import style from './EditableText.module.scss';
export default function EditableText(props) {
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
const [text, setText] = useState(defaultValue || '');
@@ -1,4 +1,4 @@
@use '../../styles/main' as *;
@use '../../../theme/main' as *;
.block {
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 { forgivingStringToMillis } from '../utils/dateConfig';
import { LoggingContext } from '../../app/context/LoggingContext';
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';
export default function EditableTimer(props) {
@@ -1,4 +1,4 @@
@use '../../styles/main'as *;
@use '../../../theme/main'as *;
.editable,
.delayedEditable {
@@ -1,6 +1,6 @@
import React from 'react';
import { clamp } from 'app/utils/math';
import { clamp } from 'common/utils/math';
import PropTypes from 'prop-types';
import styles from './MyProgressBar.module.scss';
export default function MyProgressBar(props) {
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.progress,
.progressCountdown {
+21 -39
View File
@@ -1,12 +1,18 @@
import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useState } from 'react';
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 { Image } from '@chakra-ui/react';
import { IoContract } from '@react-icons/all-files/io5/IoContract';
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 */
const navButtonStyle = {
@@ -19,6 +25,7 @@ const navButtonStyle = {
export default function NavLogo(props) {
const { isHidden } = props;
const [showNav, setShowNav] = useState(false);
const { isFullScreen, toggleFullScreen } = useFullscreen();
const handleClick = useCallback(() => {
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(() => {
// attach the event listener
document.addEventListener('keydown', handleKeyPress);
@@ -65,6 +62,7 @@ export default function NavLogo(props) {
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
whileHover={{ opacity: 1 }}
className={style.navContainer}
data-testid='nav-logo'
>
<Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
<AnimatePresence>
@@ -79,8 +77,8 @@ export default function NavLogo(props) {
>
<IconButton
aria-label='Toggle Fullscreen'
icon={<IoExpand />}
onClick={toggleFullscreen}
icon={isFullScreen ? <IoContract /> : <IoExpand />}
onClick={toggleFullScreen}
{...navButtonStyle}
/>
</motion.div>
@@ -91,27 +89,11 @@ export default function NavLogo(props) {
exit={{ opacity: 0, scaleY: 0, y: -50 }}
className={style.nav}
>
<Link to='/timer' {...tabProps}>
Timer
</Link>
<Link to='/minimal' {...tabProps}>
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>
{navigatorConstants.map((route) => (
<Link to={route.url} key={route.url} {...tabProps}>
{route.label}
</Link>
))}
</motion.div>
</>
)}
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.navContainer {
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 TodayItem from './TodayItem';
import { useInterval } from 'app/hooks/useInterval';
import { useEffect, useState } from 'react';
import { useInterval } from 'common/hooks/useInterval';
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) {
const {
@@ -67,7 +70,7 @@ export default function Paginator(props) {
}
return (
<div className={style.entries}>
<div className='paginator entries'>
{page.map((e) => {
if (e.id === selectedId) selectedState = 1;
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 PropTypes from 'prop-types';
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { IconButton } from '@chakra-ui/button';
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
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';
export default function ProtectRoute({ children }) {
@@ -1,5 +1,5 @@
@use '../../../styles/main' as *;
@use '../../../styles/mixins' as *;
@use '../../../theme/main' as *;
@use '../../../theme/mixins' as *;
.container {
background: $bg-black;
@@ -1,12 +1,12 @@
import React from 'react';
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
import PropTypes from 'prop-types';
import style from './Empty.module.scss';
export default function Empty(props) {
const { text, ...rest } = props;
const { text, dark, ...rest } = props;
return (
<div className={style.emptyContainer} {...rest}>
<div className={`${style.emptyContainer} ${dark ? style.dark : ''}`} {...rest}>
<Emptyimage className={style.empty} />
<span className={style.text}>{text}</span>
</div>
@@ -15,4 +15,5 @@ export default function Empty(props) {
Empty.propTypes = {
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 style from './TitleCard.module.scss';
export default function TitleCard(props) {
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.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 { useFetch } from '../hooks/useFetch';
import { createContext, useCallback, useEffect, useState } from 'react';
import { APP_SETTINGS } from '../api/apiConstants';
import { getSettings } from '../api/ontimeApi';
import { useFetch } from '../hooks/useFetch';
export const AppContext = createContext({
auth: false,
@@ -1,4 +1,5 @@
import React, { createContext, useCallback } from 'react';
import { createContext, useCallback } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
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';
export const CursorContext = createContext({
@@ -1,20 +1,47 @@
import React, { createContext, useCallback, useEffect, useState } from 'react';
import { useSocket } from './socketContext';
import { generateId } from '../../common/utils/generate_id';
import { nowInMillis, stringFromMillis } from '../../common/utils/time';
import { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
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: [],
emitInfo: () => undefined,
emitWarning: () => undefined,
emitError: () => undefined,
clearLog: () => undefined,
emitInfo: notInitialised,
emitWarning: notInitialised,
emitError: notInitialised,
clearLog: notInitialised
});
export const LoggingProvider = ({ children }) => {
export const LoggingProvider = ({ children }: LoggingProviderProps) => {
const MAX_MESSAGES = 100;
const socket = useSocket();
const [logData, setLogData] = useState([]);
const [logData, setLogData] = useState<Log[]>([]);
const origin = 'USER';
// handle incoming messages
@@ -24,7 +51,7 @@ export const LoggingProvider = ({ children }) => {
// Ask for log data
socket.emit('get-logger');
socket.on('logger', (data) => {
socket.on('logger', (data: Log) => {
setLogData((l) => [data, ...l]);
});
@@ -41,9 +68,9 @@ export const LoggingProvider = ({ children }) => {
* @private
*/
const _send = useCallback(
(text, level) => {
(text: string, level: LOG_LEVEL) => {
if (socket != null) {
const m = {
const m: Log = {
id: generateId(),
origin,
time: stringFromMillis(nowInMillis()),
@@ -54,7 +81,7 @@ export const LoggingProvider = ({ children }) => {
socket.emit('logger', m);
}
if (logData.length > MAX_MESSAGES) {
setLogData((l) => l.pop());
setLogData((l) => l.slice(1));
}
},
[logData, socket]
@@ -65,7 +92,7 @@ export const LoggingProvider = ({ children }) => {
* @param text
*/
const emitInfo = useCallback(
(text) => {
(text: string) => {
_send(text, 'INFO');
},
[_send]
@@ -76,7 +103,7 @@ export const LoggingProvider = ({ children }) => {
* @param text
*/
const emitWarning = useCallback(
(text) => {
(text: string) => {
_send(text, 'WARN');
},
[_send]
@@ -87,7 +114,7 @@ export const LoggingProvider = ({ children }) => {
* @param text
*/
const emitError = useCallback(
(text) => {
(text: string) => {
_send(text, 'ERROR');
},
[_send]
@@ -1,4 +1,5 @@
import React, { createContext, useCallback, useState } from 'react';
import { createContext, useCallback, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
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;
/**
@@ -10,7 +11,6 @@ export const useFetch = (namespace, fn) => {
const { data, status, isError, refetch } = useQuery(namespace, fn, {
refetchInterval: refetchIntervalMs,
cacheTime: Infinity,
notifyOnChangeProps: 'tracked',
});
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';
/**
@@ -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