Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 723b1cfa48 | |||
| f119fb4529 | |||
| 562cb769c5 | |||
| d86afee477 | |||
| 70436059d8 | |||
| fc0cbf6134 | |||
| 3128f5a195 | |||
| 94b03312a6 | |||
| b7e758bf24 | |||
| e40d750673 | |||
| b384284175 | |||
| 0e450cb6cb | |||
| 0651777055 | |||
| a8ce6f3fc1 | |||
| b80ae543a4 | |||
| 9b5d536378 | |||
| 05be0c1e95 | |||
| 18f3f3643b | |||
| a757058511 | |||
| a20909b882 | |||
| 91a0d975b3 | |||
| 97e48fc56e | |||
| 5c3e9e6a2e | |||
| 8ece6b141c | |||
| 3edb534e90 | |||
| a9ee0c3f1d | |||
| f4c5a2bf87 | |||
| dcc534f92b | |||
| 2ee4986dcf | |||
| c1de3190e9 | |||
| 1f001cf188 | |||
| 323d365f1a | |||
| dda92ba3a8 | |||
| c576b6308e | |||
| 5802e7cd03 | |||
| 86957593bb |
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,6 +32,7 @@ server/src/preloaded-db/db.json
|
||||
server/src/models/db.json
|
||||
TODO.md
|
||||
ontime-db/
|
||||
ontime-external/
|
||||
|
||||
# vscode stuff
|
||||
.vscode/*
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
build
|
||||
@@ -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)$"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard-scss",
|
||||
"stylelint-config-prettier"
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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' },
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
};
|
||||