mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 22:49:18 +00:00
Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bc6c3e852 | |||
| 257f2259e0 | |||
| 8ae3bb2df0 | |||
| ff1496d29f | |||
| 04455c08ef | |||
| 6c86ece1ff | |||
| 9a2fa527c6 | |||
| 165920b9f2 | |||
| ee3b3c0735 | |||
| ecaf0a209b | |||
| 71dace669b | |||
| 67c448572c | |||
| 53963a9ad7 | |||
| 5355e45b80 | |||
| 12f81c63ce | |||
| 7f78fac162 | |||
| e8b5cb6750 | |||
| 212e5f9e68 | |||
| 1890fc49d7 | |||
| 47a519bac1 | |||
| 083b376113 | |||
| 5b20a47ff6 | |||
| c3880cd5a7 | |||
| 91fd275564 | |||
| cf08bfc796 | |||
| 7eb9ee8d03 | |||
| ec8952be56 | |||
| 6a5020424b | |||
| 39711c1edd | |||
| 3c83f4d15b | |||
| 4c77d26a71 | |||
| 5de40e8679 | |||
| 32d70b3cd5 | |||
| 86cec56a3d | |||
| f6557a9814 | |||
| c146954a2f | |||
| 2df3d376ad | |||
| 74e1ae609c | |||
| e748bff0dd | |||
| b4ad533e6a | |||
| 0cd2ca4191 | |||
| cb8ba776e2 | |||
| c7328a33f6 | |||
| fc5800bc21 | |||
| 3c1be1fc10 | |||
| a2950442d8 | |||
| ee121bcfcb | |||
| 4234334fbb | |||
| 7f63cde7a5 | |||
| f965db148c | |||
| e6bb52ad9c | |||
| 6d067f3d3a | |||
| 506f9a868b | |||
| adb8981191 | |||
| a0b3e56896 | |||
| 229fff05a4 | |||
| 71092afc02 | |||
| 5b7045e6a5 | |||
| 7cc4364a63 | |||
| b1ba47a80f | |||
| 9611722a6d | |||
| 92cefa1331 | |||
| b4e73ff6b5 | |||
| 9031051465 | |||
| a15a9671d2 | |||
| ecd13fdc5a | |||
| 3fb862628c | |||
| 1f3133d4f3 | |||
| c0fb865959 | |||
| 64573aff8a | |||
| 655e9e8fb5 | |||
| bac2dc6b87 | |||
| 65a5aaa665 | |||
| c889d00e6b | |||
| 3753d51d85 | |||
| cb23b83b9d | |||
| 068ec800f4 | |||
| 1a8b35a5ea | |||
| a567fdc446 | |||
| 3d9a9a2226 | |||
| 9a4d659b04 | |||
| a5d8b91f34 | |||
| 5409c0ac6e | |||
| 3998b8927d | |||
| 7b9f64a630 | |||
| 40bede200c | |||
| c5c0401ac4 | |||
| f3e2944c55 | |||
| 09f2874784 | |||
| 96280e5932 | |||
| b2c01a141a | |||
| dc4ba4012c | |||
| d1e201ebb5 | |||
| a463cb491b | |||
| 6ffc314513 | |||
| 8afffb9870 | |||
| f2632412aa | |||
| 062dc0c914 | |||
| fe9f12a14b | |||
| 3144888eb1 | |||
| 820eacde91 | |||
| 6886a3236f | |||
| bb541c551f | |||
| 33575c551a | |||
| 3173fb57fc | |||
| 7d33019f53 | |||
| 6526b32d4c | |||
| 3196bd7802 | |||
| ce0657cc6d | |||
| 44c2b1b5e3 | |||
| d6aef5ea31 | |||
| 8756b396cc | |||
| 5a2f711eab | |||
| a56e2b1c1c | |||
| da829dc09f | |||
| c4d65dbe5f | |||
| 2586b0b10c | |||
| abe27d54a2 | |||
| 1343818917 | |||
| a93cd3e9b1 | |||
| 813eb9ad86 | |||
| 34e9b1ef07 | |||
| a67b89190d | |||
| ecd151a0a8 | |||
| 4cbc09864c | |||
| 20d5d8b129 | |||
| 11db39ee73 | |||
| edb79d5819 | |||
| 715fa66444 | |||
| 27f6d10677 | |||
| f2504072ce | |||
| 9e285ee8f3 | |||
| 1ccf2108eb | |||
| db56ac8049 | |||
| 0660f014c8 | |||
| 41c26778cb | |||
| 804e4ad33e | |||
| b11041938d | |||
| 5be60ff6c3 | |||
| bf8ebe942e | |||
| bdd216d881 | |||
| e6b4f537af | |||
| 797a0edf57 | |||
| c4886a617c | |||
| a79ae7e0c4 | |||
| 8cabb347e9 | |||
| 22d89c6fbc | |||
| facce4f096 | |||
| afa62a6e38 | |||
| 1b55dbc170 | |||
| 2316bbebac | |||
| 884ab0b67b | |||
| 58239af8bb | |||
| c59e070076 | |||
| fad8d2a933 | |||
| 54def8b820 | |||
| 536e447eaa | |||
| 1ca3b9c134 | |||
| a41fe8806b | |||
| 140daef7e7 | |||
| cbed3e4fe4 | |||
| 9da7613450 | |||
| a7980ef2ab | |||
| 45fe669f0a | |||
| 0ce3449083 | |||
| de715b3d3e | |||
| 6012cb7bd1 | |||
| 2c47d90f34 | |||
| 943dd19c09 | |||
| 138be478ca | |||
| 98bd320cbd | |||
| f57faf1837 | |||
| 44f025b359 | |||
| 0555d0f400 | |||
| 1e90ce4c1d | |||
| 2f09d27813 | |||
| a01046b479 | |||
| 78d5d442cf | |||
| 7c234466f7 | |||
| 29e5c96d44 | |||
| 36b1effaaf | |||
| a84c0654e2 | |||
| b34fe71995 | |||
| 2f74293505 | |||
| e2aabe3646 | |||
| 7545f6f7d0 | |||
| 958c68b6f6 | |||
| a0c5375376 | |||
| 9708f0bfc6 | |||
| 13eca98133 | |||
| 126e31403e | |||
| 53beea2768 | |||
| 0bb09dd039 | |||
| 93fb48ea1c | |||
| 1de3e01216 |
@@ -0,0 +1 @@
|
|||||||
|
"ONTIME_VERSION.js"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"root": true,
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2020
|
"ecmaVersion": 2020
|
||||||
},
|
},
|
||||||
@@ -6,21 +7,46 @@
|
|||||||
"es6": true,
|
"es6": true,
|
||||||
"jest": true
|
"jest": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"parser": "@typescript-eslint/parser",
|
||||||
"eslint:recommended"
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "eslint-config-prettier"],
|
||||||
],
|
"plugins": ["@typescript-eslint", "prettier"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": [
|
"files": ["e2e/**/**.spec.ts", "e2e/**/**.test.ts"],
|
||||||
"e2e/**/**.spec.ts",
|
"extends": ["plugin:playwright/playwright-test"]
|
||||||
"e2e/**/**.test.ts"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"plugin:playwright/playwright-test"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "warn"
|
"no-useless-concat": "warn",
|
||||||
|
"prefer-template": "warn",
|
||||||
|
"no-console": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allow": ["warn", "error"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"patterns": ["ontime-types/src/*", "ontime-utils/src/*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"caughtErrorsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prettier/prettier": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,9 +23,9 @@ jobs:
|
|||||||
version: 16.16.0
|
version: 16.16.0
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2.2.4
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7.26.3
|
version: 8
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -44,7 +44,8 @@ jobs:
|
|||||||
- name: Docker Setup Buildx
|
- name: Docker Setup Buildx
|
||||||
uses: docker/setup-buildx-action@v2.5.0
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
|
|
||||||
- name: Build and push Docker images
|
- name: Build and push stable release
|
||||||
|
if: github.event.release.prerelease == false
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -54,3 +55,14 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||||
|
|
||||||
|
- name: Build and push pre-release
|
||||||
|
if: github.event.release.prerelease == true
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||||
|
# Push is a shorthand for --output=type=registry
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:nightly
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18.18.2
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2.2.4
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7.26.3
|
version: 8
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -49,12 +49,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18.18.2
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2.2.4
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7.26.3
|
version: 8
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -82,12 +82,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18.18.2
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2.2.4
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7.26.3
|
version: 8
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -17,26 +17,50 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18.18.2
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2.2.4
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7.26.3
|
version: 8
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Run code quality per package
|
||||||
|
- name: React - Run linter + TypeScript checks
|
||||||
|
if: always()
|
||||||
|
run: pnpm lint && tsc --noEmit
|
||||||
|
working-directory: ./apps/client
|
||||||
|
|
||||||
|
- name: Server - Run linter + TypeScript checks
|
||||||
|
if: always()
|
||||||
|
run: pnpm lint && tsc --noEmit
|
||||||
|
working-directory: ./apps/server
|
||||||
|
|
||||||
|
- name: Utils - Run linter + TypeScript checks
|
||||||
|
if: always()
|
||||||
|
run: pnpm lint && tsc --noEmit
|
||||||
|
working-directory: ./packages/utils
|
||||||
|
|
||||||
|
- name: Types - Run linter
|
||||||
|
if: always()
|
||||||
|
run: pnpm lint
|
||||||
|
working-directory: ./packages/types
|
||||||
|
|
||||||
# We choose to run tests separately
|
# We choose to run tests separately
|
||||||
- name: React - Run unit tests
|
- name: React - Run unit tests
|
||||||
|
if: always()
|
||||||
run: pnpm test:pipeline
|
run: pnpm test:pipeline
|
||||||
working-directory: ./apps/client
|
working-directory: ./apps/client
|
||||||
|
|
||||||
- name: Server - Run unit tests
|
- name: Server - Run unit tests
|
||||||
|
if: always()
|
||||||
run: pnpm test:pipeline
|
run: pnpm test:pipeline
|
||||||
working-directory: ./apps/server
|
working-directory: ./apps/server
|
||||||
|
|
||||||
- name: Utils - Run unit tests
|
- name: Utils - Run unit tests
|
||||||
|
if: always()
|
||||||
run: pnpm test:pipeline
|
run: pnpm test:pipeline
|
||||||
working-directory: ./packages/utils
|
working-directory: ./packages/utils
|
||||||
|
|
||||||
@@ -49,12 +73,12 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18.18.2
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2.2.4
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7.26.3
|
version: 8
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -74,3 +98,9 @@ jobs:
|
|||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: automated-screenshots
|
||||||
|
path: automated-screenshots/
|
||||||
|
retention-days: 14
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ ontime.code-workspace
|
|||||||
test-results
|
test-results
|
||||||
playwright-report
|
playwright-report
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
automated-screenshots
|
||||||
|
|
||||||
# production
|
# production
|
||||||
build/
|
build/
|
||||||
|
|||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm lint-staged
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
build
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
playwright-report
|
||||||
|
|
||||||
|
**/*.toml
|
||||||
|
**/*.yml
|
||||||
|
**/*.json
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
|
|||||||
+6
-3
@@ -9,8 +9,8 @@ Ontime consists of 3 distinct parts
|
|||||||
The steps below will assume you have locally installed the necessary dependencies.
|
The steps below will assume you have locally installed the necessary dependencies.
|
||||||
Other dependencies will be installed as part of the setup
|
Other dependencies will be installed as part of the setup
|
||||||
|
|
||||||
- __node__ (>=16.16)
|
- __node__ (~18.18.2)
|
||||||
- __pnpm__ (>=7)
|
- __pnpm__ (>=8)
|
||||||
- __docker__ (only necessary to run and build docker images)
|
- __docker__ (only necessary to run and build docker images)
|
||||||
|
|
||||||
## LOCAL DEVELOPMENT
|
## LOCAL DEVELOPMENT
|
||||||
@@ -56,6 +56,8 @@ E2E tests are in a separate package. On running, [playwright](https://playwright
|
|||||||
webserver to test against
|
webserver to test against
|
||||||
These tests also run against a separate version of the DB (test-db)
|
These tests also run against a separate version of the DB (test-db)
|
||||||
|
|
||||||
|
Before running the E2E, you should first build the project with `pnpm build:local`.
|
||||||
|
|
||||||
You can run playwright tests from project root with `pnpm e2e`
|
You can run playwright tests from project root with `pnpm e2e`
|
||||||
|
|
||||||
When writing tests, it can be handy to run playwright in interactive mode with `pnpm e2e:i`. You would need to manually
|
When writing tests, it can be handy to run playwright in interactive mode with `pnpm e2e:i`. You would need to manually
|
||||||
@@ -74,7 +76,7 @@ You can generate a distribution for your OS by running the following steps.
|
|||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- __Install the project dependencies__ by running `pnpm i`
|
||||||
- __Build the UI and server__ by running `turbo build:local`
|
- __Build the UI and server__ by running `turbo build:electron`
|
||||||
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
|
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
|
||||||
|
|
||||||
The build distribution assets will be at `.apps/electron/dist`
|
The build distribution assets will be at `.apps/electron/dist`
|
||||||
@@ -87,6 +89,7 @@ While it should allow for a generic setup, it might need to be modified to fit y
|
|||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- __Install the project dependencies__ by running `pnpm i`
|
||||||
|
- __Build packages__ by running `pnpm build:localdocker`
|
||||||
- __Build docker image from__ by running `docker build -t getontime/ontime`
|
- __Build docker image from__ by running `docker build -t getontime/ontime`
|
||||||
- __Run docker image from compose__ by running `docker-compose up -d`
|
- __Run docker image from compose__ by running `docker-compose up -d`
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
## Download the latest releases here
|
## Download the latest releases here
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-around">
|
<div style="display: flex; justify-content: space-around">
|
||||||
<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/aux-images/mac-download.png"/></a>
|
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/mac-download.png"/></a>
|
||||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/win-download.png"/></a>
|
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/win-download.png"/></a>
|
||||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/linux-download.png"/></a>
|
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/linux-download.png"/></a>
|
||||||
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/dockerhub.png"/></a>
|
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/dockerhub.png"/></a>
|
||||||
@@ -17,7 +17,8 @@ Ontime is an application for creating and managing event running order and timer
|
|||||||
The user inputs a list of events along with scheduling and event information.
|
The user inputs a list of events along with scheduling and event information.
|
||||||
This will then populate a series of screens which are available to be rendered by any device in the Network.
|
This will then populate a series of screens which are available to be rendered by any device in the Network.
|
||||||
|
|
||||||
This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video outputs.
|
This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video
|
||||||
|
outputs.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ IP.ADDRESS:4001/public > Public / Foyer view
|
|||||||
IP.ADDRESS:4001/lower > Lower Thirds
|
IP.ADDRESS:4001/lower > Lower Thirds
|
||||||
IP.ADDRESS:4001/studio > Studio Clock
|
IP.ADDRESS:4001/studio > Studio Clock
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
For management views
|
For management views
|
||||||
-------------------------------------------------------------
|
-------------------------------------------------------------
|
||||||
@@ -63,14 +65,14 @@ More documentation is available [in our docs](https://ontime.gitbook.io)
|
|||||||
|
|
||||||
- [x] Distribute data over network and render it in the browser
|
- [x] Distribute data over network and render it in the browser
|
||||||
- [x] Different screen types
|
- [x] Different screen types
|
||||||
- Stage Timer
|
- Stage Timer
|
||||||
- Minimal Timer
|
- Minimal Timer
|
||||||
- Clock
|
- Clock
|
||||||
- Backstage Info
|
- Backstage Info
|
||||||
- Public Info
|
- Public Info
|
||||||
- Studio Clock
|
- Studio Clock
|
||||||
- Countdown
|
- Countdown
|
||||||
- [Make your own?](#make-your-own-viewer)
|
- [Make your own?](#make-your-own-viewer)
|
||||||
- [x] Configurable Lower Thirds
|
- [x] Configurable Lower Thirds
|
||||||
- [x] Collaborative editing with the cuesheet view
|
- [x] Collaborative editing with the cuesheet view
|
||||||
- [x] Send live messages to different screen types
|
- [x] Send live messages to different screen types
|
||||||
@@ -83,17 +85,19 @@ More documentation is available [in our docs](https://ontime.gitbook.io)
|
|||||||
- [x] Roll mode: run standalone using the system clock
|
- [x] Roll mode: run standalone using the system clock
|
||||||
- [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally
|
- [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally
|
||||||
- [x] [Countdown to anything!](https://ontime.gitbook.io/v2/views/countdown): have
|
- [x] [Countdown to anything!](https://ontime.gitbook.io/v2/views/countdown): have
|
||||||
a countdown to any scheduled event
|
a countdown to any scheduled event
|
||||||
- [x] Multi-platform (available on Windows, MacOS and Linux)
|
- [x] Multi-platform (available on Windows, MacOS and Linux)
|
||||||
- [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime)
|
- [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime)
|
||||||
|
|
||||||
## Unopinionated
|
## Unopinionated
|
||||||
|
|
||||||
We want Ontime to be unique by targeting freelancers instead of roles.
|
We want Ontime to be unique by targeting freelancers instead of roles.
|
||||||
|
|
||||||
We believe most freelancers work in different fields and we want to give you a tool that you can leverage across your many environments and workflows.
|
We believe most freelancers work in different fields and we want to give you a tool that you can leverage across your
|
||||||
|
many environments and workflows.
|
||||||
|
|
||||||
We are not interested in forcing workflows and have made Ontime so, it is flexible to whichever way you would like to work.
|
We are not interested in forcing workflows and have made Ontime so, it is flexible to whichever way you would like to
|
||||||
|
work.
|
||||||
|
|
||||||
## Rich APIs for workflow integrations
|
## Rich APIs for workflow integrations
|
||||||
|
|
||||||
@@ -113,7 +117,8 @@ Taking advantage of the integrations, we currently use Ontime with:
|
|||||||
|
|
||||||
Ontime broadcasts its data over WebSockets. This allows you to consume its data outside the application.
|
Ontime broadcasts its data over WebSockets. This allows you to consume its data outside the application.
|
||||||
|
|
||||||
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language that can run in the browser).
|
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language
|
||||||
|
that can run in the browser).
|
||||||
<br />
|
<br />
|
||||||
See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on
|
See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on
|
||||||
how to get you started and read the docs about
|
how to get you started and read the docs about
|
||||||
@@ -121,26 +126,11 @@ the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/ontime-api
|
|||||||
|
|
||||||
### Headless run️
|
### Headless run️
|
||||||
|
|
||||||
You can self-host and run Ontime in a docker image. The run command will:
|
You can self-host and run Ontime in a docker image.
|
||||||
|
|
||||||
- expose the necessary ports (listed in the Dockerfile)
|
The docker image along with documentation is [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
|
||||||
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
|
|
||||||
- the image name __getontime/ontime__
|
|
||||||
|
|
||||||
The docker image is
|
If you want to run this image in a Raspberry Pi, please see [the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi)
|
||||||
in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull getontime/ontime
|
|
||||||
```
|
|
||||||
|
|
||||||
and use the included docker compose to get started
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
Related information available [in the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi)
|
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
@@ -148,7 +138,8 @@ Related information available [in the docs](https://ontime.gitbook.io/v2/additio
|
|||||||
|
|
||||||
Several features are planned in the roadmap, and we continuously adjust this to match how users interact with the app.
|
Several features are planned in the roadmap, and we continuously adjust this to match how users interact with the app.
|
||||||
<br />
|
<br />
|
||||||
Have an idea? Reach out via [email](mail@getontime.no) or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
Have an idea? Reach out via [email](mail@getontime.no)
|
||||||
|
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
||||||
|
|
||||||
### Issues
|
### Issues
|
||||||
|
|
||||||
@@ -159,21 +150,20 @@ Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
|
|||||||
|
|
||||||
When installing the app you would see warning screens from the Operating System like:
|
When installing the app you would see warning screens from the Operating System like:
|
||||||
|
|
||||||
```Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.```
|
`Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.`
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
```Ontime can't be opened because it is from an unidentified developer```
|
`Ontime can't be opened because it is from an unidentified developer`
|
||||||
|
|
||||||
or in Linux
|
or in Linux
|
||||||
|
|
||||||
```Could Not Display "ontime-linux.AppImage```
|
`Could Not Display "ontime-linux.AppImage`
|
||||||
|
|
||||||
You can circumvent this by allowing the execution of the app manually.
|
You can circumvent this by allowing the execution of the app manually.
|
||||||
|
|
||||||
- In Windows: click more and select "Run Anyway"
|
- In Windows: click more and select "Run Anyway"
|
||||||
- in macOS: after attempting to run the installer, navigate to System Preferences -> Security &
|
- in macOS: the solution in macOS is different across versions, please refer to the [apple documentation](https://support.apple.com/en-gb/guide/mac-help/mh40616/mac)
|
||||||
Privacy and allow the execution of the app
|
|
||||||
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
|
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
|
||||||
File as a Program
|
File as a Program
|
||||||
|
|
||||||
@@ -185,6 +175,7 @@ please [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
|||||||
#### Safari
|
#### Safari
|
||||||
|
|
||||||
There are known issues with Safari versions lower than 13:
|
There are known issues with Safari versions lower than 13:
|
||||||
|
|
||||||
- Spacing and text styles might have small inconsistencies
|
- Spacing and text styles might have small inconsistencies
|
||||||
- Table view does not work
|
- Table view does not work
|
||||||
|
|
||||||
@@ -194,7 +185,8 @@ There is no plan for any further work on this.
|
|||||||
|
|
||||||
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
|
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
If you are a developer and would like to contribute with some code, please open an issue to discuss before opening a Pull Request.
|
If you are a developer and would like to contribute with some code, please open an issue to discuss before opening a
|
||||||
|
Pull Request.
|
||||||
<br />
|
<br />
|
||||||
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
|
Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md)
|
||||||
|
|
||||||
|
|||||||
+2
-18
@@ -8,34 +8,18 @@
|
|||||||
"browser": true,
|
"browser": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
"plugin:react-hooks/recommended",
|
"plugin:react-hooks/recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@tanstack/eslint-plugin-query/recommended"
|
||||||
"eslint-config-prettier",
|
|
||||||
"plugin:@tanstack/eslint-plugin-query/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react",
|
"react",
|
||||||
"testing-library",
|
"testing-library",
|
||||||
"simple-import-sort",
|
"simple-import-sort",
|
||||||
"@tanstack/query",
|
"@tanstack/query"
|
||||||
"@typescript-eslint",
|
|
||||||
"prettier"
|
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
|
||||||
"prettier/prettier": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"endOfLine": "auto"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-useless-concat": "warn",
|
|
||||||
"prefer-template": "warn",
|
|
||||||
"react/jsx-no-bind": [
|
"react/jsx-no-bind": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
|
|||||||
+36
-34
@@ -1,36 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "2.7.2",
|
"version": "3.0.0-alpha",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.7.0",
|
"@chakra-ui/react": "^2.7.0",
|
||||||
"@dnd-kit/core": "^6.0.8",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/sortable": "^7.0.2",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.1",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@sentry/react": "^7.46.0",
|
"@sentry/react": "^7.92.0",
|
||||||
"@sentry/tracing": "^7.46.0",
|
"@tanstack/react-query": "^5.17.9",
|
||||||
"@tanstack/react-query": "^4.28.0",
|
"@tanstack/react-query-devtools": "^5.17.9",
|
||||||
"@tanstack/react-query-devtools": "^4.29.0",
|
"@tanstack/react-table": "^8.11.3",
|
||||||
"@tanstack/react-table": "^8.9.2",
|
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"csv-stringify": "^6.2.3",
|
"csv-stringify": "^6.4.5",
|
||||||
"deepmerge": "^4.3.0",
|
|
||||||
"framer-motion": "^10.10.0",
|
"framer-motion": "^10.10.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-fast-compare": "^3.2.0",
|
"react-fast-compare": "^3.2.2",
|
||||||
"react-hook-form": "^7.43.5",
|
"react-hook-form": "^7.49.2",
|
||||||
"react-qr-code": "^2.0.11",
|
"react-qr-code": "^2.0.12",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"typeface-open-sans": "^1.1.13",
|
"typeface-open-sans": "^1.1.13",
|
||||||
"web-vitals": "^3.1.1",
|
"web-vitals": "^3.1.1",
|
||||||
"zustand": "^4.3.6"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
@@ -38,11 +37,15 @@
|
|||||||
"dev": "cross-env BROWSER=none vite",
|
"dev": "cross-env BROWSER=none vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:local": "cross-env NODE_ENV=local vite build",
|
"build:local": "cross-env NODE_ENV=local vite build",
|
||||||
|
"build:electron": "cross-env NODE_ENV=local vite build",
|
||||||
"build:docker": "vite build",
|
"build:docker": "vite build",
|
||||||
"lint": "eslint .",
|
"build:localdocker": "cross-env NODE_ENV=local vite build",
|
||||||
|
"lint": "eslint . --quiet",
|
||||||
|
"lint-staged": "eslint",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:pipeline": "vitest run",
|
"test:pipeline": "vitest run",
|
||||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build"
|
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build",
|
||||||
|
"analyse": "npx vite-bundle-visualizer"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -57,23 +60,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/vite-plugin": "^0.4.0",
|
"@sentry/vite-plugin": "^2.14.0",
|
||||||
"@tanstack/eslint-plugin-query": "^4.26.2",
|
"@tanstack/eslint-plugin-query": "^5.8.4",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.1.1",
|
"@testing-library/react": "^13.1.1",
|
||||||
"@testing-library/user-event": "^14.1.1",
|
"@testing-library/user-event": "^14.1.1",
|
||||||
"@types/color": "^3.0.3",
|
"@types/color": "^3.0.3",
|
||||||
"@types/luxon": "^3.2.0",
|
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@types/testing-library__jest-dom": "^5.14.5",
|
"@types/testing-library__jest-dom": "^5.14.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^6.10.0",
|
||||||
"@vitejs/plugin-react": "^3.0.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.53.0",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-jest": "^27.1.7",
|
"eslint-plugin-jest": "^27.6.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"eslint-plugin-react": "^7.32.0",
|
"eslint-plugin-react": "^7.32.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
@@ -81,13 +83,13 @@
|
|||||||
"jsdom": "^21.1.0",
|
"jsdom": "^21.1.0",
|
||||||
"ontime-types": "workspace:*",
|
"ontime-types": "workspace:*",
|
||||||
"ontime-utils": "workspace:*",
|
"ontime-utils": "workspace:*",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^3.0.3",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.3.1",
|
"vite": "^5.1.0",
|
||||||
"vite-plugin-compression2": "^0.9.0",
|
"vite-plugin-compression2": "^0.12.0",
|
||||||
"vite-plugin-svgr": "^2.4.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"vite-tsconfig-paths": "^4.2.0",
|
"vite-tsconfig-paths": "^4.3.1",
|
||||||
"vitest": "^0.30.1"
|
"vitest": "^1.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import withData from './features/viewers/ViewWrapper';
|
|||||||
|
|
||||||
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
|
const Editor = lazy(() => import('./features/editors/ProtectedEditor'));
|
||||||
const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
|
const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet'));
|
||||||
|
const Operator = lazy(() => import('./features/operator/Operator'));
|
||||||
|
|
||||||
const TimerView = lazy(() => import('./features/viewers/timer/Timer'));
|
const TimerView = lazy(() => import('./features/viewers/timer/Timer'));
|
||||||
const MinimalTimerView = lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
|
const MinimalTimerView = lazy(() => import('./features/viewers/minimal-timer/MinimalTimer'));
|
||||||
@@ -14,7 +15,7 @@ const Countdown = lazy(() => import('./features/viewers/countdown/Countdown'));
|
|||||||
|
|
||||||
const Backstage = lazy(() => import('./features/viewers/backstage/Backstage'));
|
const Backstage = lazy(() => import('./features/viewers/backstage/Backstage'));
|
||||||
const Public = lazy(() => import('./features/viewers/public/Public'));
|
const Public = lazy(() => import('./features/viewers/public/Public'));
|
||||||
const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper'));
|
const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerThird'));
|
||||||
const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock'));
|
const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock'));
|
||||||
|
|
||||||
const STimer = withAlias(withData(TimerView));
|
const STimer = withAlias(withData(TimerView));
|
||||||
@@ -30,27 +31,20 @@ const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper'
|
|||||||
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
|
const RundownPanel = lazy(() => import('./features/rundown/RundownExport'));
|
||||||
const TimerControl = lazy(() => import('./features/control/playback/TimerControlExport'));
|
const TimerControl = lazy(() => import('./features/control/playback/TimerControlExport'));
|
||||||
const MessageControl = lazy(() => import('./features/control/message/MessageControlExport'));
|
const MessageControl = lazy(() => import('./features/control/message/MessageControlExport'));
|
||||||
const Info = lazy(() => import('./features/info/InfoExport'));
|
|
||||||
|
|
||||||
export default function AppRouter() {
|
export default function AppRouter() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<Navigate to='/timer' />} />
|
<Route path='/' element={<Navigate to='/timer' />} />
|
||||||
<Route path='/speaker' element={<STimer />} />
|
|
||||||
<Route path='/presenter' element={<STimer />} />
|
|
||||||
<Route path='/stage' element={<STimer />} />
|
|
||||||
<Route path='/timer' element={<STimer />} />
|
<Route path='/timer' element={<STimer />} />
|
||||||
|
|
||||||
<Route path='/minimal' element={<SMinimalTimer />} />
|
<Route path='/minimal' element={<SMinimalTimer />} />
|
||||||
<Route path='/minimalTimer' element={<SMinimalTimer />} />
|
|
||||||
<Route path='/simpleTimer' element={<SMinimalTimer />} />
|
|
||||||
|
|
||||||
<Route path='/clock' element={<SClock />} />
|
<Route path='/clock' element={<SClock />} />
|
||||||
|
|
||||||
<Route path='/countdown' element={<SCountdown />} />
|
<Route path='/countdown' element={<SCountdown />} />
|
||||||
|
|
||||||
<Route path='/sm' element={<SBackstage />} />
|
|
||||||
<Route path='/backstage' element={<SBackstage />} />
|
<Route path='/backstage' element={<SBackstage />} />
|
||||||
|
|
||||||
<Route path='/public' element={<SPublic />} />
|
<Route path='/public' element={<SPublic />} />
|
||||||
@@ -58,11 +52,12 @@ export default function AppRouter() {
|
|||||||
{/*/!* Lower cannot have fallback *!/*/}
|
{/*/!* Lower cannot have fallback *!/*/}
|
||||||
<Route path='/lower' element={<SLowerThird />} />
|
<Route path='/lower' element={<SLowerThird />} />
|
||||||
|
|
||||||
|
<Route path='/op' element={<Operator />} />
|
||||||
|
<Route path='/operator' element={<Operator />} />
|
||||||
|
|
||||||
{/*/!* Protected Routes *!/*/}
|
{/*/!* Protected Routes *!/*/}
|
||||||
<Route path='/editor' element={<Editor />} />
|
<Route path='/editor' element={<Editor />} />
|
||||||
<Route path='/cuesheet' element={<Cuesheet />} />
|
<Route path='/cuesheet' element={<Cuesheet />} />
|
||||||
<Route path='/cuelist' element={<Cuesheet />} />
|
|
||||||
<Route path='/table' element={<Cuesheet />} />
|
|
||||||
|
|
||||||
{/*/!* Protected Routes - Elements *!/*/}
|
{/*/!* Protected Routes - Elements *!/*/}
|
||||||
<Route
|
<Route
|
||||||
@@ -89,14 +84,6 @@ export default function AppRouter() {
|
|||||||
</EditorFeatureWrapper>
|
</EditorFeatureWrapper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path='/info'
|
|
||||||
element={
|
|
||||||
<EditorFeatureWrapper>
|
|
||||||
<Info />
|
|
||||||
</EditorFeatureWrapper>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{/*/!* Send to default if nothing found *!/*/}
|
{/*/!* Send to default if nothing found *!/*/}
|
||||||
<Route path='*' element={<STimer />} />
|
<Route path='*' element={<STimer />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,25 +1,28 @@
|
|||||||
// REST stuff
|
// REST stuff
|
||||||
export const EVENT_DATA = ['eventdata'];
|
|
||||||
export const ALIASES = ['aliases'];
|
export const ALIASES = ['aliases'];
|
||||||
export const USERFIELDS = ['userFields'];
|
|
||||||
export const RUNDOWN_TABLE_KEY = 'rundown';
|
|
||||||
export const RUNDOWN_TABLE = [RUNDOWN_TABLE_KEY];
|
|
||||||
export const APP_INFO = ['appinfo'];
|
export const APP_INFO = ['appinfo'];
|
||||||
export const OSC_SETTINGS = ['oscSettings'];
|
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const HTTP_SETTINGS = ['httpSettings'];
|
||||||
|
export const OSC_SETTINGS = ['oscSettings'];
|
||||||
|
export const PROJECT_DATA = ['project'];
|
||||||
|
export const PROJECT_LIST = ['projectList'];
|
||||||
|
export const RUNDOWN = ['rundown'];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
|
export const SHEET_STATE = ['sheetState'];
|
||||||
|
export const USERFIELDS = ['userFields'];
|
||||||
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
|
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
export const isProduction = import.meta.env.MODE === 'production';
|
export const isProduction = import.meta.env.MODE === 'production';
|
||||||
|
export const isDev = !isProduction;
|
||||||
|
|
||||||
const STATIC_PORT = 4001;
|
const STATIC_PORT = 4001;
|
||||||
export const serverPort = isProduction ? location.port : STATIC_PORT;
|
export const serverPort = isProduction ? location.port : STATIC_PORT;
|
||||||
export const serverURL = `${location.protocol}//${location.hostname}:${serverPort}`;
|
export const serverURL = `${location.protocol}//${location.hostname}:${serverPort}`;
|
||||||
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
|
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
|
||||||
|
|
||||||
export const eventURL = `${serverURL}/eventdata`;
|
export const projectDataURL = `${serverURL}/project`;
|
||||||
export const rundownURL = `${serverURL}/events`;
|
export const rundownURL = `${serverURL}/events`;
|
||||||
export const ontimeURL = `${serverURL}/ontime`;
|
export const ontimeURL = `${serverURL}/ontime`;
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,32 @@ import axios, { AxiosError } from 'axios';
|
|||||||
import { LogLevel } from 'ontime-types';
|
import { LogLevel } from 'ontime-types';
|
||||||
import { generateId, millisToString } from 'ontime-utils';
|
import { generateId, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
import { addLog } from '../stores/logger';
|
import { addLog } from '../stores/logger';
|
||||||
import { nowInMillis } from '../utils/time';
|
import { nowInMillis } from '../utils/time';
|
||||||
|
|
||||||
export function logAxiosError(prepend: string, error: unknown) {
|
export function maybeAxiosError(error: unknown) {
|
||||||
let message;
|
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
const statusText = (error as AxiosError).response?.statusText ?? '';
|
const statusText = (error as AxiosError).response?.statusText ?? '';
|
||||||
const data = (error as AxiosError).response?.data ?? '';
|
let data = (error as AxiosError).response?.data ?? '';
|
||||||
message = `${prepend} ${statusText}: ${data}`;
|
if (typeof data === 'object') {
|
||||||
|
if ('message' in data) {
|
||||||
|
data = JSON.stringify(data.message);
|
||||||
|
} else {
|
||||||
|
data = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${statusText}: ${data}`;
|
||||||
} else {
|
} else {
|
||||||
message = `${prepend}: ${error}`;
|
if (typeof error !== 'string') {
|
||||||
|
return JSON.stringify(error);
|
||||||
|
}
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logAxiosError(prepend: string, error: unknown) {
|
||||||
|
const message = `${prepend}: ${maybeAxiosError(error)}`;
|
||||||
|
|
||||||
addLog({
|
addLog({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
@@ -23,3 +37,10 @@ export function logAxiosError(prepend: string, error: unknown) {
|
|||||||
text: message,
|
text: message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function invalidates react-query caches
|
||||||
|
*/
|
||||||
|
export async function invalidateAllCaches() {
|
||||||
|
await ontimeQueryClient.invalidateQueries();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { EventData } from 'ontime-types';
|
|
||||||
|
|
||||||
import { eventURL } from './apiConstants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to fetch event data
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function fetchEventData(): Promise<EventData> {
|
|
||||||
const res = await axios.get(eventURL);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to mutate event data
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function postEventData(data: EventData) {
|
|
||||||
return axios.post(eventURL, data);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
import { OntimeEvent, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||||
|
|
||||||
import { rundownURL } from './apiConstants';
|
import { rundownURL } from './apiConstants';
|
||||||
|
|
||||||
@@ -7,6 +7,16 @@ import { rundownURL } from './apiConstants';
|
|||||||
* @description HTTP request to fetch all events
|
* @description HTTP request to fetch all events
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
|
export async function fetchCachedRundown(): Promise<RundownCached> {
|
||||||
|
const res = await axios.get(`${rundownURL}/cached`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use fetchCachedRundown instead
|
||||||
|
* @description HTTP request to fetch all events
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
export async function fetchRundown(): Promise<OntimeRundown> {
|
export async function fetchRundown(): Promise<OntimeRundown> {
|
||||||
const res = await axios.get(rundownURL);
|
const res = await axios.get(rundownURL);
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -16,7 +26,7 @@ export async function fetchRundown(): Promise<OntimeRundown> {
|
|||||||
* @description HTTP request to post new event
|
* @description HTTP request to post new event
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export async function requestPostEvent(data: OntimeRundownEntry) {
|
export async function requestPostEvent(data: Partial<OntimeRundownEntry>) {
|
||||||
return axios.post(rundownURL, data);
|
return axios.post(rundownURL, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +38,19 @@ export async function requestPutEvent(data: Partial<OntimeRundownEntry>) {
|
|||||||
return axios.put(rundownURL, data);
|
return axios.put(rundownURL, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BatchEditEntry = {
|
||||||
|
data: Partial<OntimeEvent>;
|
||||||
|
ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to put multiple events
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export async function requestBatchPutEvents(data: BatchEditEntry) {
|
||||||
|
return axios.put(`${rundownURL}/batchEdit`, data);
|
||||||
|
}
|
||||||
|
|
||||||
export type ReorderEntry = {
|
export type ReorderEntry = {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
from: number;
|
from: number;
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
import axios from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { Alias, EventData, OSCSettings, OscSubscription, Settings, UserFields, ViewSettings } from 'ontime-types';
|
import {
|
||||||
|
Alias,
|
||||||
|
DatabaseModel,
|
||||||
|
GetInfo,
|
||||||
|
HttpSettings,
|
||||||
|
MessageResponse,
|
||||||
|
OntimeRundown,
|
||||||
|
OSCSettings,
|
||||||
|
ProjectData,
|
||||||
|
ProjectFileListResponse,
|
||||||
|
Settings,
|
||||||
|
UserFields,
|
||||||
|
ViewSettings,
|
||||||
|
} from 'ontime-types';
|
||||||
|
import { ExcelImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiRepoLatest } from '../../externals';
|
import { apiRepoLatest } from '../../externals';
|
||||||
import { InfoType } from '../models/Info';
|
import fileDownload from '../utils/fileDownload';
|
||||||
|
|
||||||
import { ontimeURL } from './apiConstants';
|
import { ontimeURL } from './apiConstants';
|
||||||
|
|
||||||
@@ -27,7 +41,7 @@ export async function postSettings(data: Settings) {
|
|||||||
* @description HTTP request to retrieve application info
|
* @description HTTP request to retrieve application info
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export async function getInfo(): Promise<InfoType> {
|
export async function getInfo(): Promise<GetInfo> {
|
||||||
const res = await axios.get(`${ontimeURL}/info`);
|
const res = await axios.get(`${ontimeURL}/info`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
@@ -59,13 +73,29 @@ export async function getAliases(): Promise<Alias[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to mutate aliases
|
* @description HTTP request to create an alias
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export async function postAliases(data: Alias[]) {
|
export async function postAlias(data: Alias) {
|
||||||
return axios.post(`${ontimeURL}/aliases`, data);
|
return axios.post(`${ontimeURL}/aliases`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to update aliases
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export async function updateAliases(data: Partial<Alias>) {
|
||||||
|
return axios.put(`${ontimeURL}/aliases`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to delete alias
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export async function deleteAlias(alias: string) {
|
||||||
|
return axios.delete(`${ontimeURL}/aliases/${alias}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to retrieve user fields
|
* @description HTTP request to retrieve user fields
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
@@ -96,58 +126,64 @@ export async function getOSC(): Promise<OSCSettings> {
|
|||||||
* @description HTTP request to mutate osc settings
|
* @description HTTP request to mutate osc settings
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export async function postOSC(data: OSCSettings) {
|
export async function postOSC(data: OSCSettings): Promise<AxiosResponse<OSCSettings>> {
|
||||||
return axios.post(`${ontimeURL}/osc`, data);
|
return axios.post(`${ontimeURL}/osc`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to mutate osc subscriptions
|
* @description HTTP request to retrieve http settings
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export async function postOscSubscriptions(data: OscSubscription) {
|
export async function getHTTP(): Promise<HttpSettings> {
|
||||||
return axios.post(`${ontimeURL}/osc-subscriptions`, data);
|
const res = await axios.get(`${ontimeURL}/http`);
|
||||||
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to download db
|
* @description HTTP request to mutate http settings
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export const downloadRundown = async () => {
|
export async function postHTTP(data: HttpSettings): Promise<AxiosResponse<HttpSettings>> {
|
||||||
await axios({
|
return axios.post(`${ontimeURL}/http`, data);
|
||||||
url: `${ontimeURL}/db`,
|
}
|
||||||
method: 'GET',
|
|
||||||
responseType: 'blob', // important
|
|
||||||
}).then((response) => {
|
|
||||||
const headerLine = response.headers['Content-Disposition'];
|
|
||||||
let filename = 'rundown.json';
|
|
||||||
|
|
||||||
// try and get the filename from the response
|
/**
|
||||||
if (headerLine != null) {
|
* @description HTTP request to download db in CSV format
|
||||||
const startFileNameIndex = headerLine.indexOf('"') + 1;
|
*/
|
||||||
const endFileNameIndex = headerLine.lastIndexOf('"');
|
export const downloadCSV = (fileName?: string) => {
|
||||||
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
return fileDownload(ontimeURL, { name: fileName ?? 'rundown', type: 'csv' }, { type: 'text/csv;charset=utf-8;' });
|
||||||
}
|
};
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
|
/**
|
||||||
const link = document.createElement('a');
|
* @description HTTP request to download db in JSON format
|
||||||
link.href = url;
|
*/
|
||||||
link.setAttribute('download', filename);
|
export const downloadRundown = (fileName?: string) => {
|
||||||
document.body.appendChild(link);
|
return fileDownload(
|
||||||
link.click();
|
ontimeURL,
|
||||||
});
|
{ name: fileName ?? 'rundown', type: 'json' },
|
||||||
|
{ type: 'application/json;charset=utf-8;' },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: should this be extracted to shared code?
|
||||||
|
export type ProjectFileImportOptions = {
|
||||||
|
onlyRundown: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to upload events db
|
* @description HTTP request to upload events db
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
type UploadDataOptions = {
|
export const uploadProjectFile = async (
|
||||||
onlyRundown?: boolean;
|
file: File,
|
||||||
};
|
setProgress: (value: number) => void,
|
||||||
export const uploadData = async (file: File, setProgress: (value: number) => void, options?: UploadDataOptions) => {
|
options?: Partial<ProjectFileImportOptions>,
|
||||||
|
) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('userFile', file);
|
formData.append('userFile', file);
|
||||||
const onlyRundown = options?.onlyRundown || 'false';
|
|
||||||
|
const onlyRundown = Boolean(options?.onlyRundown);
|
||||||
|
|
||||||
await axios
|
await axios
|
||||||
.post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, {
|
.post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -161,6 +197,46 @@ export const uploadData = async (file: File, setProgress: (value: number) => voi
|
|||||||
.then((response) => response.data.id);
|
.then((response) => response.data.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Make patch changes to the objects in the db
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export async function patchData(patchDb: Partial<DatabaseModel>) {
|
||||||
|
const response = await axios.patch(`${ontimeURL}/db`, patchDb);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostPreviewExcelResponse = {
|
||||||
|
rundown: OntimeRundown;
|
||||||
|
userFields: UserFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Make patch changes to the objects in the db
|
||||||
|
* @return {Promise} - returns parsed rundown and userfields
|
||||||
|
*/
|
||||||
|
export async function postPreviewExcel(file: File, setProgress: (value: number) => void, options?: ExcelImportMap) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('userFile', file);
|
||||||
|
formData.append('options', JSON.stringify(options));
|
||||||
|
|
||||||
|
const response: AxiosResponse<PostPreviewExcelResponse> = await axios.post(
|
||||||
|
`${ontimeURL}/preview-spreadsheet`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
const complete = progressEvent?.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0;
|
||||||
|
setProgress(complete);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
export type HasUpdate = {
|
export type HasUpdate = {
|
||||||
url: string;
|
url: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -178,6 +254,143 @@ export async function getLatestVersion(): Promise<HasUpdate> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postNew(initialData: Partial<EventData>) {
|
/**
|
||||||
return axios.post(`${ontimeURL}/new`, initialData);
|
* @description HTTP request to get the list of available project files
|
||||||
|
*/
|
||||||
|
export async function getProjects(): Promise<ProjectFileListResponse> {
|
||||||
|
const res = await axios.get(`${ontimeURL}/projects`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to load a project file
|
||||||
|
*/
|
||||||
|
export async function loadProject(filename: string): Promise<MessageResponse> {
|
||||||
|
const res = await axios.post(`${ontimeURL}/load-project`, {
|
||||||
|
filename,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 1
|
||||||
|
*/
|
||||||
|
export const uploadSheetClientFile = async (file: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('userFile', file);
|
||||||
|
const res = await axios
|
||||||
|
.post(`${ontimeURL}/sheet/clientsecret`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => response.data.id);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 1 test
|
||||||
|
*/
|
||||||
|
export const getClientSecrect = async () => {
|
||||||
|
const response = await axios.get(`${ontimeURL}/sheet/clientsecret`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 2
|
||||||
|
*/
|
||||||
|
export const getSheetsAuthUrl = async () => {
|
||||||
|
const response = await axios.get(`${ontimeURL}/sheet/authentication/url`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 2 test
|
||||||
|
*/
|
||||||
|
export const getAuthentication = async () => {
|
||||||
|
const response = await axios.get(`${ontimeURL}/sheet/authentication`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 3
|
||||||
|
* @returns worksheetOptions
|
||||||
|
*/
|
||||||
|
export const postId = async (id: string) => {
|
||||||
|
const response = await axios.post(`${ontimeURL}/sheet/id`, { id });
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 4
|
||||||
|
*/
|
||||||
|
export const postWorksheet = async (id: string, worksheet: string) => {
|
||||||
|
const response = await axios.post(`${ontimeURL}/sheet/worksheet`, { id, worksheet });
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 5
|
||||||
|
*/
|
||||||
|
export const postPreviewSheet = async (id: string, options: ExcelImportMap) => {
|
||||||
|
const response = await axios.post(`${ontimeURL}/sheet/pull`, { id, options });
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 5
|
||||||
|
*/
|
||||||
|
export const postPushSheet = async (id: string, options: ExcelImportMap) => {
|
||||||
|
const response = await axios.post(`${ontimeURL}/sheet-push`, { id, options });
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to rename a project file
|
||||||
|
*/
|
||||||
|
export async function renameProject(filename: string, newFilename: string): Promise<MessageResponse> {
|
||||||
|
const url = `${ontimeURL}/project/${filename}/rename`;
|
||||||
|
const decodedUrl = decodeURIComponent(url);
|
||||||
|
const res = await axios.put(decodedUrl, {
|
||||||
|
newFilename,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to duplicate a project file
|
||||||
|
*/
|
||||||
|
export async function duplicateProject(filename: string, newFilename: string): Promise<MessageResponse> {
|
||||||
|
const url = `${ontimeURL}/project/${filename}/duplicate`;
|
||||||
|
const decodedUrl = decodeURIComponent(url);
|
||||||
|
const res = await axios.post(decodedUrl, {
|
||||||
|
newFilename,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to delete a project file
|
||||||
|
*/
|
||||||
|
export async function deleteProject(filename: string): Promise<MessageResponse> {
|
||||||
|
const url = `${ontimeURL}/project/${filename}`;
|
||||||
|
const decodedUrl = decodeURIComponent(url);
|
||||||
|
const res = await axios.delete(decodedUrl);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to create a project file
|
||||||
|
*/
|
||||||
|
export async function createProject(
|
||||||
|
project: Partial<
|
||||||
|
ProjectData & {
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
): Promise<MessageResponse> {
|
||||||
|
const url = `${ontimeURL}/project`;
|
||||||
|
const decodedUrl = decodeURIComponent(url);
|
||||||
|
const res = await axios.post(decodedUrl, project);
|
||||||
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
|
import { projectDataURL } from './apiConstants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to fetch project data
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export async function getProjectData(): Promise<ProjectData> {
|
||||||
|
const res = await axios.get(projectDataURL);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to mutate project data
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export async function postProjectData(data: ProjectData) {
|
||||||
|
return axios.post(projectDataURL, data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
@use "../../../theme/mixins" as *;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
@include action-link;
|
||||||
|
font-size: $inner-section-text-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkIcon {
|
||||||
|
margin-left: $element-inner-spacing;
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { MouseEvent, ReactNode } from 'react';
|
||||||
|
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||||
|
|
||||||
|
import { openLink } from '../../utils/linkUtils';
|
||||||
|
|
||||||
|
import style from './AppLink.module.scss';
|
||||||
|
|
||||||
|
interface AppLinkProps {
|
||||||
|
href: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppLink(props: AppLinkProps) {
|
||||||
|
const { href, children } = props;
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
openLink(href);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href='#!' target='_blank' rel='noreferrer' className={style.link} onClick={handleClick}>
|
||||||
|
{children} <IoArrowUp className={style.linkIcon} />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,14 +10,12 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FiPower } from '@react-icons/all-files/fi/FiPower';
|
import { IoPowerOutline } from '@react-icons/all-files/io5/IoPowerOutline';
|
||||||
|
|
||||||
import { Size } from '../../models/Util.type';
|
|
||||||
import { useEmitLog } from '../../stores/logger';
|
import { useEmitLog } from '../../stores/logger';
|
||||||
|
|
||||||
interface QuitIconBtnProps {
|
interface QuitIconBtnProps {
|
||||||
clickHandler: () => void;
|
clickHandler: () => void;
|
||||||
size?: Size;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +40,7 @@ const quitBtnStyle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function QuitIconBtn(props: QuitIconBtnProps) {
|
export default function QuitIconBtn(props: QuitIconBtnProps) {
|
||||||
const { clickHandler, size = 'lg', disabled, ...rest } = props;
|
const { clickHandler, disabled, ...rest } = props;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { emitInfo } = useEmitLog();
|
const { emitInfo } = useEmitLog();
|
||||||
const onClose = () => setIsOpen(false);
|
const onClose = () => setIsOpen(false);
|
||||||
@@ -67,8 +65,8 @@ export default function QuitIconBtn(props: QuitIconBtnProps) {
|
|||||||
<Tooltip label='Quit Application'>
|
<Tooltip label='Quit Application'>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Quit Application'
|
aria-label='Quit Application'
|
||||||
size={size}
|
size='lg'
|
||||||
icon={<FiPower />}
|
icon={<IoPowerOutline />}
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
{...quitBtnStyle}
|
{...quitBtnStyle}
|
||||||
@@ -81,7 +79,7 @@ export default function QuitIconBtn(props: QuitIconBtnProps) {
|
|||||||
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
||||||
Ontime Shutdown
|
Ontime Shutdown
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogBody>This will shutdown the program and all running servers. Are you sure?</AlertDialogBody>
|
<AlertDialogBody>This will shutdown the Ontime server. Are you sure?</AlertDialogBody>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<Button ref={cancelRef} onClick={onClose} variant='ghost'>
|
<Button ref={cancelRef} onClick={onClose} variant='ghost'>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MouseEvent } from 'react';
|
|||||||
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
|
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
|
||||||
|
|
||||||
interface TooltipActionBtnProps extends IconButtonProps {
|
interface TooltipActionBtnProps extends IconButtonProps {
|
||||||
clickHandler: (event?: MouseEvent) => void | Promise<void>;
|
clickHandler: (event: MouseEvent) => void | Promise<void>;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
openDelay?: number;
|
openDelay?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
@use '../../../theme/v2Styles' as *;
|
|
||||||
|
|
||||||
.header {
|
|
||||||
font-size: $inner-section-text-size;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: $section-white;
|
|
||||||
margin-top: $main-spacing;
|
|
||||||
border-bottom: 1px solid $border-color-ondark;
|
|
||||||
padding-bottom: $element-inner-spacing;
|
|
||||||
margin-bottom: $element-spacing;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moreExpanded {
|
|
||||||
transform: scaleY(-1);
|
|
||||||
transition: transform $transition-time-feedback;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moreCollapsed {
|
|
||||||
transform: scaleY(1);
|
|
||||||
transition: transform $transition-time-feedback;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
|
||||||
|
|
||||||
import style from './CollapseBar.module.scss';
|
|
||||||
|
|
||||||
interface CollapseBarProps {
|
|
||||||
title: string;
|
|
||||||
isCollapsed: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CollapseBar(props: CollapseBarProps) {
|
|
||||||
const { title = 'Collapse bar', isCollapsed, onClick } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.header} onClick={onClick}>
|
|
||||||
{title}
|
|
||||||
<FiChevronUp className={isCollapsed ? style.moreCollapsed : style.moreExpanded} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
.contextMenuButton {
|
.contextMenuButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextMenuBackdrop {
|
.contextMenuBackdrop {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// logic (with some modifications) culled from:
|
// logic (with some modifications) culled from:
|
||||||
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
||||||
|
|
||||||
import { Fragment, ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import { Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react';
|
import { Menu, MenuButton, MenuGroup, MenuList } from '@chakra-ui/react';
|
||||||
import { IconType } from '@react-icons/all-files';
|
import { IconType } from '@react-icons/all-files';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { ContextMenuOption } from './ContextMenuOption';
|
||||||
|
|
||||||
import style from './ContextMenu.module.scss';
|
import style from './ContextMenu.module.scss';
|
||||||
|
|
||||||
type ContextMenuCoords = {
|
type ContextMenuCoords = {
|
||||||
@@ -13,14 +15,23 @@ type ContextMenuCoords = {
|
|||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Option = {
|
export type OptionWithoutGroup = {
|
||||||
label: string;
|
label: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
icon: IconType;
|
icon: IconType;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
withDivider?: boolean;
|
withDivider?: boolean;
|
||||||
isDisabled?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OptionWithGroup = {
|
||||||
|
label: string;
|
||||||
|
group: Omit<OptionWithoutGroup, 'isGroup'>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Option = OptionWithoutGroup | OptionWithGroup;
|
||||||
|
|
||||||
|
const isOptionWithGroup = (option: Option): option is OptionWithGroup => 'group' in option;
|
||||||
|
|
||||||
type ContextMenuStore = {
|
type ContextMenuStore = {
|
||||||
coords: ContextMenuCoords;
|
coords: ContextMenuCoords;
|
||||||
options: Option[];
|
options: Option[];
|
||||||
@@ -69,14 +80,17 @@ export const ContextMenu = ({ children }: ContextMenuProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
{options.map(({ label, icon: Icon, onClick, withDivider, isDisabled }, i) => (
|
{options.map((option) =>
|
||||||
<Fragment key={label}>
|
isOptionWithGroup(option) ? (
|
||||||
{withDivider && <MenuDivider />}
|
<MenuGroup key={option.label} title={option.label}>
|
||||||
<MenuItem key={i} icon={<Icon />} onClick={onClick} isDisabled={isDisabled}>
|
{option.group.map((groupOption) => (
|
||||||
{label}
|
<ContextMenuOption key={groupOption.label} {...groupOption} />
|
||||||
</MenuItem>
|
))}
|
||||||
</Fragment>
|
</MenuGroup>
|
||||||
))}
|
) : (
|
||||||
|
<ContextMenuOption key={option.label} {...option} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { MenuDivider, MenuItem } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { OptionWithoutGroup } from './ContextMenu';
|
||||||
|
|
||||||
|
export const ContextMenuOption = ({ label, onClick, isDisabled, icon: Icon, withDivider }: OptionWithoutGroup) => (
|
||||||
|
<>
|
||||||
|
{withDivider && <MenuDivider />}
|
||||||
|
<MenuItem icon={<Icon />} onClick={onClick} isDisabled={isDisabled}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.delaySymbol {
|
||||||
|
svg {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: $ontime-delay;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
|
import { IoChevronDown } from '@react-icons/all-files/io5/IoChevronDown';
|
||||||
|
import { IoChevronUp } from '@react-icons/all-files/io5/IoChevronUp';
|
||||||
|
|
||||||
|
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||||
|
import { millisToDelayString } from '../../utils/dateConfig';
|
||||||
|
|
||||||
|
import style from './DelayIndicator.module.scss';
|
||||||
|
|
||||||
|
interface DelayIndicatorProps {
|
||||||
|
delayValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DelayIndicator(props: DelayIndicatorProps) {
|
||||||
|
const { delayValue } = props;
|
||||||
|
|
||||||
|
if (typeof delayValue === 'number') {
|
||||||
|
if (delayValue < 0) {
|
||||||
|
return (
|
||||||
|
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue, true)}>
|
||||||
|
<span className={style.delaySymbol}>
|
||||||
|
<IoChevronDown />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayValue > 0) {
|
||||||
|
return (
|
||||||
|
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue, true)}>
|
||||||
|
<span className={style.delaySymbol}>
|
||||||
|
<IoChevronUp />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
|
||||||
import { runtime } from '@/common/stores/runtime';
|
import { runtimeStore } from '@/common/stores/runtime';
|
||||||
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
|
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
|
||||||
|
|
||||||
import style from './ErrorBoundary.module.scss';
|
import style from './ErrorBoundary.module.scss';
|
||||||
@@ -23,13 +23,13 @@ class ErrorBoundary extends React.Component {
|
|||||||
|
|
||||||
componentDidCatch(error, info) {
|
componentDidCatch(error, info) {
|
||||||
this.setState({
|
this.setState({
|
||||||
error: error,
|
error,
|
||||||
errorInfo: info,
|
errorInfo: info,
|
||||||
});
|
});
|
||||||
|
|
||||||
Sentry.withScope((scope) => {
|
Sentry.withScope((scope) => {
|
||||||
scope.setExtras('error', error);
|
scope.setExtras('error', error);
|
||||||
scope.setExtras('store', runtime.getState());
|
scope.setExtras('store', runtimeStore.getState());
|
||||||
scope.setExtras('hasSocket', { hasConnected, shouldReconnect, reconnectAttempts });
|
scope.setExtras('hasSocket', { hasConnected, shouldReconnect, reconnectAttempts });
|
||||||
const eventId = Sentry.captureException(error);
|
const eventId = Sentry.captureException(error);
|
||||||
this.setState({ eventId, info });
|
this.setState({ eventId, info });
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@use '../../../theme/v2Styles' as *;
|
|
||||||
|
|
||||||
.errorContainer {
|
.errorContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
color: $blue-500;
|
||||||
|
transition-property: color;
|
||||||
|
transition-duration: $transition-time-action;
|
||||||
|
|
||||||
|
&.inline {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $ontime-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { MouseEvent, ReactNode } from 'react';
|
||||||
|
import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
|
||||||
|
|
||||||
|
import { openLink } from '../../utils/linkUtils';
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
import style from './ExternalLink.module.scss';
|
||||||
|
|
||||||
|
interface ExternalLinkProps {
|
||||||
|
href: string;
|
||||||
|
children: ReactNode;
|
||||||
|
inline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExternalLink(props: ExternalLinkProps) {
|
||||||
|
const { href, inline, children } = props;
|
||||||
|
const classes = cx([style.link, inline ? style.inline : null]);
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
openLink(href);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href='#!' target='_blank' rel='noreferrer' className={classes} onClick={handleClick}>
|
||||||
|
{children} <IoOpenOutline />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import { Textarea, TextareaProps } from '@chakra-ui/react';
|
|||||||
import autosize from 'autosize/dist/autosize';
|
import autosize from 'autosize/dist/autosize';
|
||||||
|
|
||||||
export const AutoTextArea = (props: TextareaProps) => {
|
export const AutoTextArea = (props: TextareaProps) => {
|
||||||
|
const { value } = props;
|
||||||
|
|
||||||
const ref = useRef<HTMLTextAreaElement>(null);
|
const ref = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -12,7 +14,7 @@ export const AutoTextArea = (props: TextareaProps) => {
|
|||||||
return () => {
|
return () => {
|
||||||
autosize.destroy(node);
|
autosize.destroy(node);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
|
|
||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem
|
||||||
}
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
aspect-ratio: 1;
|
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
border-radius: 16px;
|
aspect-ratio: 1;
|
||||||
border: 4px solid #262626;
|
border-radius: 99px;
|
||||||
|
border: 2px solid $gray-1200;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
border: 2px solid #578AF4;
|
border: 2px solid $blue-500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.center {
|
.center {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
color: #578AF4;
|
color: $ui-white;
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { TitleActions } from '../../../../features/event-editor/composite/EventEditorDataLeft';
|
|
||||||
|
|
||||||
import Swatch from './Swatch';
|
import Swatch from './Swatch';
|
||||||
|
|
||||||
import style from './SwatchSelect.module.scss';
|
import style from './SwatchSelect.module.scss';
|
||||||
|
|
||||||
interface ColourInputProps {
|
interface ColourInputProps {
|
||||||
value: string;
|
value: string;
|
||||||
name: TitleActions;
|
name: 'colour';
|
||||||
handleChange: (newValue: TitleActions, name: string) => void;
|
handleChange: (newValue: 'colour', name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colours = [
|
const colours = [
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@use '../../../../theme/v2Styles' as *;
|
|
||||||
|
|
||||||
$input-font-size: 15px;
|
$input-font-size: 15px;
|
||||||
|
|
||||||
.delayInput {
|
.delayInput {
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
@use "../../../../theme/_ontimeColours" as *;
|
|
||||||
@use "../../../../theme/_v2Styles" as *;
|
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Input, Textarea } from '@chakra-ui/react';
|
import { Input, Textarea } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
|
import { EventEditorSubmitActions } from '../../../../features/rundown/event-editor/EventEditor';
|
||||||
import { Size } from '../../../models/Util.type';
|
import { Size } from '../../../models/Util.type';
|
||||||
|
|
||||||
import useReactiveTextInput from './useReactiveTextInput';
|
import useReactiveTextInput from './useReactiveTextInput';
|
||||||
@@ -19,9 +19,11 @@ interface TextInputProps extends BaseProps {
|
|||||||
isTextArea?: false;
|
isTextArea?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResizeOptions = 'horizontal' | 'vertical' | 'none';
|
||||||
|
|
||||||
interface TextAreaProps extends BaseProps {
|
interface TextAreaProps extends BaseProps {
|
||||||
isTextArea: true;
|
isTextArea: true;
|
||||||
resize?: 'horizontal' | 'vertical' | 'none';
|
resize?: ResizeOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputProps = TextInputProps | TextAreaProps;
|
type InputProps = TextInputProps | TextAreaProps;
|
||||||
@@ -35,7 +37,7 @@ export default function TextInput(props: InputProps) {
|
|||||||
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
||||||
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
||||||
|
|
||||||
let resize = 'none';
|
let resize: ResizeOptions = 'none';
|
||||||
if (isTextArea) {
|
if (isTextArea) {
|
||||||
resize = (props as TextAreaProps)?.resize ?? 'none';
|
resize = (props as TextAreaProps)?.resize ?? 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,4 @@
|
|||||||
@use "../../../../theme/v2Styles" as *;
|
|
||||||
|
|
||||||
$input-font-size: 15px;
|
|
||||||
$input-delayed-border-color: #E69056;
|
|
||||||
|
|
||||||
.timeInput {
|
.timeInput {
|
||||||
width: fit-content !important;
|
|
||||||
|
|
||||||
.inputLeft {
|
|
||||||
max-width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputLeft,
|
|
||||||
.inputButton {
|
|
||||||
aspect-ratio: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputField {
|
|
||||||
font-size: $input-font-size;
|
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
width: 7.5em;
|
width: 6.5em;
|
||||||
padding: 0 0 0 2.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warn {
|
|
||||||
&::after {
|
|
||||||
content: "*";
|
|
||||||
color: $warning-orange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.delayed {
|
|
||||||
.inputField {
|
|
||||||
border: 1px solid $input-delayed-border-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,23 @@
|
|||||||
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
import { Input } from '@chakra-ui/react';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
|
||||||
import { useEmitLog } from '../../../stores/logger';
|
import { useEmitLog } from '../../../stores/logger';
|
||||||
import { forgivingStringToMillis } from '../../../utils/dateConfig';
|
import { forgivingStringToMillis } from '../../../utils/dateConfig';
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
import { TimeEntryField } from '../../../utils/timesManager';
|
|
||||||
|
|
||||||
import style from './TimeInput.module.scss';
|
import style from './TimeInput.module.scss';
|
||||||
|
interface TimeInputProps<T extends string> {
|
||||||
interface TimeInputProps {
|
name: T;
|
||||||
id?: TimeEntryField;
|
submitHandler: (field: T, value: string) => void;
|
||||||
name: TimeEntryField;
|
|
||||||
submitHandler: (field: TimeEntryField, value: number) => void;
|
|
||||||
time?: number;
|
time?: number;
|
||||||
delay?: number;
|
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
validationHandler: (entry: TimeEntryField, val: number) => boolean;
|
disabled?: boolean;
|
||||||
previousEnd?: number;
|
className?: string;
|
||||||
warning?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ButtonInitial(name: TimeEntryField) {
|
export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
|
||||||
if (name === 'timeStart') return 'S';
|
const { name, submitHandler, time = 0, placeholder, disabled, className } = props;
|
||||||
if (name === 'timeEnd') return 'E';
|
|
||||||
if (name === 'durationOverride') return 'D';
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function ButtonTooltip(name: TimeEntryField, warning?: string) {
|
|
||||||
if (name === 'timeStart') return `Start${warning ? `: ${warning}` : ''}`;
|
|
||||||
if (name === 'timeEnd') return `End${warning ? `: ${warning}` : ''}`;
|
|
||||||
if (name === 'durationOverride') return `Duration${warning ? `: ${warning}` : ''}`;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TimeInput(props: TimeInputProps) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
submitHandler,
|
|
||||||
time = 0,
|
|
||||||
delay = 0,
|
|
||||||
placeholder,
|
|
||||||
validationHandler,
|
|
||||||
previousEnd = 0,
|
|
||||||
warning,
|
|
||||||
} = props;
|
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
@@ -83,35 +53,20 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newValMillis = 0;
|
// we dont know the values in the rundown, escalate to handler
|
||||||
|
if (newValue.startsWith('p') || newValue.startsWith('+')) {
|
||||||
// check for known aliases
|
submitHandler(name, newValue);
|
||||||
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
|
|
||||||
// string to pass should be the time of the end before
|
|
||||||
if (previousEnd != null) {
|
|
||||||
newValMillis = previousEnd;
|
|
||||||
}
|
|
||||||
} else if (newValue.startsWith('+') || newValue.startsWith('p+') || newValue.startsWith('p +')) {
|
|
||||||
// string to pass should add to the end before
|
|
||||||
const val = newValue.substring(1);
|
|
||||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
|
||||||
} else {
|
|
||||||
// convert entered value to milliseconds
|
|
||||||
newValMillis = forgivingStringToMillis(newValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if time is different from before
|
const valueInMillis = forgivingStringToMillis(newValue);
|
||||||
if (newValMillis === time) return false;
|
if (valueInMillis === time) {
|
||||||
|
return false;
|
||||||
// validate with parent
|
}
|
||||||
if (!validationHandler(name, newValMillis)) return false;
|
|
||||||
|
|
||||||
// update entry
|
|
||||||
submitHandler(name, newValMillis);
|
|
||||||
|
|
||||||
|
submitHandler(name, newValue);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[name, previousEnd, submitHandler, time, validationHandler],
|
[name, submitHandler, time],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,15 +76,11 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
const validateAndSubmit = useCallback(
|
const validateAndSubmit = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
const success = handleSubmit(newValue);
|
const success = handleSubmit(newValue);
|
||||||
if (success) {
|
if (!success) {
|
||||||
const ms = forgivingStringToMillis(newValue);
|
|
||||||
const delayed = name === 'timeEnd' ? Math.max(0, ms + delay) : Math.max(0, ms + delay);
|
|
||||||
setValue(millisToString(delayed));
|
|
||||||
} else {
|
|
||||||
resetValue();
|
resetValue();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[delay, handleSubmit, name, resetValue],
|
[handleSubmit, resetValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,50 +120,26 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
resetValue();
|
resetValue();
|
||||||
}, [resetValue, time]);
|
}, [resetValue, time]);
|
||||||
|
|
||||||
const isDelayed = delay !== 0;
|
const timeInputClasses = cx([style.timeInput, className]);
|
||||||
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
|
|
||||||
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]);
|
|
||||||
|
|
||||||
const TooltipLabel = useMemo(() => {
|
|
||||||
return ButtonTooltip(name, warning);
|
|
||||||
}, [name, warning]);
|
|
||||||
|
|
||||||
const ButtonText = useMemo(() => {
|
|
||||||
return ButtonInitial(name);
|
|
||||||
}, [name]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup size='sm' className={inputClasses}>
|
<Input
|
||||||
<InputLeftElement className={style.inputLeft}>
|
disabled={disabled}
|
||||||
<Tooltip label={TooltipLabel} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
size='sm'
|
||||||
<Button
|
ref={inputRef}
|
||||||
size='sm'
|
data-testid={`time-input-${name}`}
|
||||||
variant='ontime-subtle-white'
|
className={timeInputClasses}
|
||||||
className={buttonClasses}
|
fontSize='1rem'
|
||||||
tabIndex={-1}
|
type='text'
|
||||||
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
|
placeholder={placeholder}
|
||||||
borderRight='1px solid transparent'
|
variant='ontime-filled'
|
||||||
borderRadius='2px 0 0 2px'
|
onFocus={handleFocus}
|
||||||
>
|
onChange={(event) => setValue(event.target.value)}
|
||||||
{ButtonText}
|
onBlur={onBlurHandler}
|
||||||
</Button>
|
onKeyDown={onKeyDownHandler}
|
||||||
</Tooltip>
|
value={value}
|
||||||
</InputLeftElement>
|
maxLength={8}
|
||||||
<Input
|
autoComplete='off'
|
||||||
ref={inputRef}
|
/>
|
||||||
id={id}
|
|
||||||
data-testid={`time-input-${name}`}
|
|
||||||
className={style.inputField}
|
|
||||||
type='text'
|
|
||||||
placeholder={placeholder}
|
|
||||||
variant='ontime-filled'
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onChange={(event) => setValue(event.target.value)}
|
|
||||||
onBlur={onBlurHandler}
|
|
||||||
onKeyDown={onKeyDownHandler}
|
|
||||||
value={value}
|
|
||||||
maxLength={8}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
$input-delayed-border-color: $ontime-delay-text;
|
||||||
|
|
||||||
|
.timeInput {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
&.delayed {
|
||||||
|
border: 1px solid $input-delayed-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputField {
|
||||||
|
max-width: 7.75em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { InputGroup, Tooltip } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
||||||
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
|
import TimeInput from './TimeInput';
|
||||||
|
|
||||||
|
import style from './TimeInputWithButton.module.scss';
|
||||||
|
|
||||||
|
interface TimeInputWithButtonProps<T extends string> {
|
||||||
|
name: T;
|
||||||
|
submitHandler: (field: T, value: string) => void;
|
||||||
|
time?: number;
|
||||||
|
hasDelay?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TimeInputWithButton<T extends string>(props: PropsWithChildren<TimeInputWithButtonProps<T>>) {
|
||||||
|
const { name, submitHandler, time, hasDelay, placeholder, disabled, children } = props;
|
||||||
|
|
||||||
|
const inputClasses = cx([style.timeInput, hasDelay ? style.delayed : null]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroup size='sm' className={inputClasses} width='fit-content'>
|
||||||
|
<Tooltip label={placeholder} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||||
|
<TimeInput<T>
|
||||||
|
name={name}
|
||||||
|
submitHandler={submitHandler}
|
||||||
|
time={time}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={style.inputField}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{children}
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ $progress-bar-br: 3px;
|
|||||||
border-radius: $progress-bar-br;
|
border-radius: $progress-bar-br;
|
||||||
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
|
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&--hidden {
|
&--hidden {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -31,7 +32,6 @@ $progress-bar-br: 3px;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: $progress-bar-br;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,12 +39,10 @@ $progress-bar-br: 3px;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 0 $progress-bar-br $progress-bar-br 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiprogress-bar__bg-danger {
|
.multiprogress-bar__bg-danger {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 0 $progress-bar-br $progress-bar-br 0;
|
|
||||||
}
|
}
|
||||||
+22
-10
@@ -1,14 +1,16 @@
|
|||||||
|
import { MaybeNumber } from 'ontime-types';
|
||||||
|
|
||||||
import { clamp } from '../../utils/math';
|
import { clamp } from '../../utils/math';
|
||||||
|
|
||||||
import './MultiPartProgressBar.scss';
|
import './MultiPartProgressBar.scss';
|
||||||
|
|
||||||
interface MultiPartProgressBar {
|
interface MultiPartProgressBar {
|
||||||
now: number;
|
now: MaybeNumber;
|
||||||
complete: number;
|
complete: number;
|
||||||
normalColor: string;
|
normalColor: string;
|
||||||
warning: number;
|
warning?: MaybeNumber;
|
||||||
warningColor: string;
|
warningColor: string;
|
||||||
danger: number;
|
danger?: MaybeNumber;
|
||||||
dangerColor: string;
|
dangerColor: string;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -17,17 +19,27 @@ interface MultiPartProgressBar {
|
|||||||
export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
||||||
const { now, complete, normalColor, warning, warningColor, danger, dangerColor, hidden, className = '' } = props;
|
const { now, complete, normalColor, warning, warningColor, danger, dangerColor, hidden, className = '' } = props;
|
||||||
|
|
||||||
const percentComplete = 100 - clamp(100 - (Math.max(now, 0) * 100) / complete, 0, 100);
|
const percentComplete = 100 - clamp(100 - (Math.max(now ?? 0, 0) * 100) / complete, 0, 100);
|
||||||
|
|
||||||
const dangerWidth = clamp((danger / complete) * 100, 0, 100);
|
const dangerWidth = danger ? clamp((danger / complete) * 100, 0, 100) : 0;
|
||||||
const warningWidth = clamp((warning / complete) * 100, 0, 100);
|
const warningWidth = warning ? clamp((warning / complete) * 100, 0, 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
|
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
|
||||||
<div className='multiprogress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
|
{now !== null && (
|
||||||
<div className='multiprogress-bar__bg-warning' style={{ width: `${warningWidth}%`, backgroundColor: warningColor }} />
|
<>
|
||||||
<div className='multiprogress-bar__bg-danger' style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }} />
|
<div className='multiprogress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
|
||||||
<div className='multiprogress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
<div
|
||||||
|
className='multiprogress-bar__bg-warning'
|
||||||
|
style={{ width: `${warningWidth}%`, backgroundColor: warningColor }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className='multiprogress-bar__bg-danger'
|
||||||
|
style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }}
|
||||||
|
/>
|
||||||
|
<div className='multiprogress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
@use '../../../theme/v2Styles' as *;
|
|
||||||
@use '../../../theme/mixins' as *;
|
@use '../../../theme/mixins' as *;
|
||||||
@use '../../../theme/ontimeColours' as *;
|
|
||||||
|
|
||||||
$menu-bg: $gray-1200;
|
$menu-bg: $gray-1200;
|
||||||
$menu-hover-bg: $gray-1350;
|
$menu-hover-bg: $gray-1350;
|
||||||
@@ -10,10 +8,6 @@ $icon-color: $ui-white;
|
|||||||
$button-bg: $gray-1050;
|
$button-bg: $gray-1050;
|
||||||
$button-size: 48px;
|
$button-size: 48px;
|
||||||
|
|
||||||
.mirror {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -75,7 +69,6 @@ $button-size: 48px;
|
|||||||
@include action-link;
|
@include action-link;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $menu-hover-bg;
|
background-color: $menu-hover-bg;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { KeyboardEvent, memo, useEffect, useRef, useState } from 'react';
|
import { memo, useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
import { useDisclosure } from '@chakra-ui/react';
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
@@ -12,8 +12,8 @@ import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
|||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import useClickOutside from '../../hooks/useClickOutside';
|
import useClickOutside from '../../hooks/useClickOutside';
|
||||||
import useFullscreen from '../../hooks/useFullscreen';
|
import useFullscreen from '../../hooks/useFullscreen';
|
||||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
|
||||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||||
|
import { isKeyEnter } from '../../utils/keyEvent';
|
||||||
|
|
||||||
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
||||||
|
|
||||||
@@ -23,18 +23,19 @@ function NavigationMenu() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
const { toggleMirror } = useViewOptionsStore();
|
||||||
const [showButton, setShowButton] = useState(false);
|
const [showButton, setShowButton] = useState(false);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useClickOutside(menuRef, () => setShowMenu(false));
|
useClickOutside(menuRef, () => setShowMenu(false));
|
||||||
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
const toggleMenu = () => setShowMenu((prev) => !prev);
|
const toggleMenu = () => setShowMenu((prev) => !prev);
|
||||||
useKeyDown(toggleMenu, ' ', { isDisabled: searchParams.get('edit') === 'true' || isOpen });
|
|
||||||
|
|
||||||
|
// show on mouse move
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let fadeOut: NodeJS.Timeout | null = null;
|
let fadeOut: NodeJS.Timeout | null = null;
|
||||||
const setShowMenuTrue = () => {
|
const setShowMenuTrue = () => {
|
||||||
@@ -53,17 +54,16 @@ function NavigationMenu() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
|
|
||||||
const handleFullscreen = () => toggleFullScreen();
|
const handleFullscreen = () => toggleFullScreen();
|
||||||
const handleMirror = () => toggleMirror();
|
const handleMirror = () => toggleMirror();
|
||||||
|
|
||||||
const showEditFormDrawer = () => {
|
const showEditFormDrawer = () => {
|
||||||
searchParams.append('edit', 'true');
|
searchParams.set('edit', 'true');
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
|
<div id='navigation-menu-portal' ref={menuRef}>
|
||||||
<RenameClientModal isOpen={isOpen} onClose={onClose} />
|
<RenameClientModal isOpen={isOpen} onClose={onClose} />
|
||||||
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
|
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
|
||||||
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
|
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
|
||||||
@@ -116,6 +116,10 @@ function NavigationMenu() {
|
|||||||
Cuesheet
|
Cuesheet
|
||||||
<IoArrowUp className={style.linkIcon} />
|
<IoArrowUp className={style.linkIcon} />
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link to='/op' className={style.link} tabIndex={0}>
|
||||||
|
Operator
|
||||||
|
<IoArrowUp className={style.linkIcon} />
|
||||||
|
</Link>
|
||||||
<hr className={style.separator} />
|
<hr className={style.separator} />
|
||||||
{navigatorConstants.map((route) => (
|
{navigatorConstants.map((route) => (
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ export default function RenameClientModal({ isOpen, onClose }: RenameClientModal
|
|||||||
|
|
||||||
const handleRename = async () => {
|
const handleRename = async () => {
|
||||||
if (newName) {
|
if (newName) {
|
||||||
await setClientName(newName);
|
setClientName(newName);
|
||||||
persistName(newName);
|
persistName(newName);
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -8,10 +8,12 @@ import { tooltipDelayFast } from '../../../ontimeConfig';
|
|||||||
|
|
||||||
interface PlaybackIconProps {
|
interface PlaybackIconProps {
|
||||||
state: Playback;
|
state: Playback;
|
||||||
|
skipTooltip?: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlaybackIcon(props: PlaybackIconProps) {
|
export default function PlaybackIcon(props: PlaybackIconProps) {
|
||||||
const { state } = props;
|
const { state, skipTooltip, className } = props;
|
||||||
|
|
||||||
// if timer is Pause or Armed
|
// if timer is Pause or Armed
|
||||||
let label = 'Timer Paused';
|
let label = 'Timer Paused';
|
||||||
@@ -28,9 +30,13 @@ export default function PlaybackIcon(props: PlaybackIconProps) {
|
|||||||
Icon = IoStop;
|
Icon = IoStop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skipTooltip) {
|
||||||
|
return <Icon className={className} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip openDelay={tooltipDelayFast} label={label} shouldWrapChildren>
|
<Tooltip openDelay={tooltipDelayFast} label={label} shouldWrapChildren>
|
||||||
<Icon />
|
<Icon className={className} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
@use '../../../theme/v2Styles' as *;
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import Empty from '../state/Empty';
|
|
||||||
|
|
||||||
import { useSchedule } from './ScheduleContext';
|
import { useSchedule } from './ScheduleContext';
|
||||||
import ScheduleItem from './ScheduleItem';
|
import ScheduleItem from './ScheduleItem';
|
||||||
|
|
||||||
@@ -13,8 +11,9 @@ interface ScheduleProps {
|
|||||||
export default function Schedule({ isProduction, className }: ScheduleProps) {
|
export default function Schedule({ isProduction, className }: ScheduleProps) {
|
||||||
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
|
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
|
||||||
|
|
||||||
|
// TODO: design a nice placeholder for empty schedules
|
||||||
if (paginatedEvents?.length < 1) {
|
if (paginatedEvents?.length < 1) {
|
||||||
return <Empty text='No events to show' />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedState: 'past' | 'now' | 'future' = 'past';
|
let selectedState: 'past' | 'now' | 'future' = 'past';
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { createContext, PropsWithChildren, useContext, useState } from 'react';
|
import { createContext, PropsWithChildren, useContext, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { OntimeEvent } from 'ontime-types';
|
import { OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { useInterval } from '../../hooks/useInterval';
|
import { useInterval } from '../../hooks/useInterval';
|
||||||
|
import { isStringBoolean } from '../../utils/viewUtils';
|
||||||
|
|
||||||
interface ScheduleContextState {
|
interface ScheduleContextState {
|
||||||
events: OntimeEvent[];
|
events: OntimeEvent[];
|
||||||
@@ -19,7 +21,6 @@ interface ScheduleProviderProps {
|
|||||||
events: OntimeEvent[];
|
events: OntimeEvent[];
|
||||||
selectedEventId: string | null;
|
selectedEventId: string | null;
|
||||||
isBackstage?: boolean;
|
isBackstage?: boolean;
|
||||||
eventsPerPage?: number;
|
|
||||||
time?: number;
|
time?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,16 +29,29 @@ export const ScheduleProvider = ({
|
|||||||
events,
|
events,
|
||||||
selectedEventId,
|
selectedEventId,
|
||||||
isBackstage = false,
|
isBackstage = false,
|
||||||
eventsPerPage = 7,
|
|
||||||
time = 10,
|
time = 10,
|
||||||
}: PropsWithChildren<ScheduleProviderProps>) => {
|
}: PropsWithChildren<ScheduleProviderProps>) => {
|
||||||
const [visiblePage, setVisiblePage] = useState(0);
|
const [visiblePage, setVisiblePage] = useState(0);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const numPages = Math.ceil(events.length / eventsPerPage);
|
// look for overrides from views
|
||||||
|
const hidePast = isStringBoolean(searchParams.get('hidePast'));
|
||||||
|
const stopCycle = isStringBoolean(searchParams.get('stopCycle'));
|
||||||
|
const eventsPerPage = Number(searchParams.get('eventsPerPage') ?? 7);
|
||||||
|
|
||||||
|
let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
|
||||||
|
|
||||||
|
const viewEvents = [...events];
|
||||||
|
if (hidePast) {
|
||||||
|
// we want to show the event after the next
|
||||||
|
viewEvents.splice(0, selectedEventIndex + 2);
|
||||||
|
selectedEventIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numPages = Math.ceil(viewEvents.length / eventsPerPage);
|
||||||
const eventStart = eventsPerPage * visiblePage;
|
const eventStart = eventsPerPage * visiblePage;
|
||||||
const eventEnd = eventsPerPage * (visiblePage + 1);
|
const eventEnd = eventsPerPage * (visiblePage + 1);
|
||||||
const paginatedEvents = events.slice(eventStart, eventEnd);
|
const paginatedEvents = viewEvents.slice(eventStart, eventEnd);
|
||||||
const selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
|
|
||||||
|
|
||||||
const resolveScheduleType = () => {
|
const resolveScheduleType = () => {
|
||||||
if (selectedEventIndex >= eventStart && selectedEventIndex < eventEnd) {
|
if (selectedEventIndex >= eventStart && selectedEventIndex < eventEnd) {
|
||||||
@@ -52,7 +66,9 @@ export const ScheduleProvider = ({
|
|||||||
|
|
||||||
// every SCROLL_TIME go to the next array
|
// every SCROLL_TIME go to the next array
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
if (events.length > eventsPerPage) {
|
if (stopCycle) {
|
||||||
|
setVisiblePage(0);
|
||||||
|
} else if (events.length > eventsPerPage) {
|
||||||
const next = (visiblePage + 1) % numPages;
|
const next = (visiblePage + 1) % numPages;
|
||||||
setVisiblePage(next);
|
setVisiblePage(next);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||||
import { formatTime } from '../../utils/time';
|
import { formatTime } from '../../utils/time';
|
||||||
|
|
||||||
import './Schedule.scss';
|
import './Schedule.scss';
|
||||||
|
|
||||||
|
const formatOptions = {
|
||||||
|
format12: 'hh:mm a',
|
||||||
|
format24: 'HH:mm',
|
||||||
|
};
|
||||||
|
|
||||||
interface ScheduleItemProps {
|
interface ScheduleItemProps {
|
||||||
selected: 'past' | 'now' | 'future';
|
selected: 'past' | 'now' | 'future';
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
@@ -14,19 +20,10 @@ interface ScheduleItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ScheduleItem(props: ScheduleItemProps) {
|
export default function ScheduleItem(props: ScheduleItemProps) {
|
||||||
const {
|
const { selected, timeStart, timeEnd, title, presenter, backstageEvent, colour, skip } = props;
|
||||||
selected,
|
|
||||||
timeStart,
|
|
||||||
timeEnd,
|
|
||||||
title,
|
|
||||||
presenter,
|
|
||||||
backstageEvent,
|
|
||||||
colour,
|
|
||||||
skip,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const start = formatTime(timeStart, { format: 'hh:mm' });
|
const start = formatTime(timeStart, formatOptions);
|
||||||
const end = formatTime(timeEnd, { format: 'hh:mm' });
|
const end = formatTime(timeEnd, formatOptions);
|
||||||
const userColour = colour !== '' ? colour : '';
|
const userColour = colour !== '' ? colour : '';
|
||||||
const selectStyle = `entry--${selected}`;
|
const selectStyle = `entry--${selected}`;
|
||||||
|
|
||||||
@@ -34,12 +31,15 @@ export default function ScheduleItem(props: ScheduleItemProps) {
|
|||||||
<li className={`entry ${selectStyle} ${skip ? 'skip' : ''}`}>
|
<li className={`entry ${selectStyle} ${skip ? 'skip' : ''}`}>
|
||||||
<div className='entry-times'>
|
<div className='entry-times'>
|
||||||
<span className='entry-colour' style={{ backgroundColor: userColour }} />
|
<span className='entry-colour' style={{ backgroundColor: userColour }} />
|
||||||
{`${start} → ${end} ${backstageEvent ? '*' : ''}`}
|
<div style={{ display: 'flex' }}>
|
||||||
|
<SuperscriptTime time={start} />
|
||||||
|
{' → '}
|
||||||
|
<SuperscriptTime time={end} />
|
||||||
|
{backstageEvent ? '*' : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='entry-title'>{title}</div>
|
<div className='entry-title'>{title}</div>
|
||||||
{presenter && (
|
{presenter && <div className='entry-presenter'>{presenter}</div>}
|
||||||
<div className='entry-presenter'>{presenter}</div>
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ export default function ScheduleNav({ className }: ScheduleNavProps) {
|
|||||||
<div className={`schedule-nav ${className}`}>
|
<div className={`schedule-nav ${className}`}>
|
||||||
{numPages > 1 &&
|
{numPages > 1 &&
|
||||||
[...Array(numPages).keys()].map((i) => (
|
[...Array(numPages).keys()].map((i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={i === visiblePage ? 'schedule-nav__item schedule-nav__item--selected' : 'schedule-nav__item'}
|
className={i === visiblePage ? 'schedule-nav__item schedule-nav__item--selected' : 'schedule-nav__item'}
|
||||||
/>
|
/>
|
||||||
),
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@use '../../../theme/ontimeColours' as *;
|
|
||||||
|
|
||||||
.emptyContainer {
|
.emptyContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CSSProperties } from 'react';
|
import { CSSProperties } from 'react';
|
||||||
import { ReactComponent as Emptyimage } from '@/assets/images/empty.svg';
|
|
||||||
|
import EmptyImage from '../../../assets/images/empty.svg?react';
|
||||||
|
|
||||||
import style from './Empty.module.scss';
|
import style from './Empty.module.scss';
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ export default function Empty(props: EmptyProps) {
|
|||||||
const { text, ...rest } = props;
|
const { text, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<div className={style.emptyContainer} {...rest}>
|
<div className={style.emptyContainer} {...rest}>
|
||||||
<Emptyimage className={style.empty} />
|
<EmptyImage className={style.empty} />
|
||||||
<span className={style.text}>{text}</span>
|
<span className={style.text}>{text}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { formatDisplay } from 'ontime-utils';
|
|
||||||
|
|
||||||
import './TimerDisplay.scss';
|
|
||||||
|
|
||||||
interface TimerDisplayProps {
|
|
||||||
time?: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays time in ms in formatted timetag
|
|
||||||
* @param props
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
const TimerDisplay = (props: TimerDisplayProps) => {
|
|
||||||
const { time } = props;
|
|
||||||
|
|
||||||
let display = '';
|
|
||||||
|
|
||||||
if (time === null || typeof time === 'undefined' || isNaN(time)) {
|
|
||||||
display = '-- : -- : --';
|
|
||||||
} else {
|
|
||||||
display = formatDisplay(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNegative = (time ?? 0) < 0;
|
|
||||||
|
|
||||||
const classes = `timer ${isNegative ? 'timer--finished' : ''}`;
|
|
||||||
|
|
||||||
return <div className={classes}>{display}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(TimerDisplay);
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { Input, Select, Switch } from '@chakra-ui/react';
|
import { Input, InputGroup, InputLeftElement, Select, Switch } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { isStringBoolean } from '../../utils/viewUtils';
|
import { isStringBoolean } from '../../utils/viewUtils';
|
||||||
|
|
||||||
@@ -9,16 +9,22 @@ interface EditFormInputProps {
|
|||||||
paramField: ParamField;
|
paramField: ParamField;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ParamInput({ paramField }: EditFormInputProps) {
|
export default function ParamInput(props: EditFormInputProps) {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { id, type } = paramField;
|
const { paramField } = props;
|
||||||
|
const { id, type, defaultValue } = paramField;
|
||||||
|
|
||||||
if (type === 'option') {
|
if (type === 'option') {
|
||||||
const optionFromParams = searchParams.get(id);
|
const optionFromParams = searchParams.get(id);
|
||||||
const defaultOptionValue = optionFromParams || undefined;
|
const defaultOptionValue = optionFromParams || defaultValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select placeholder='Select an option' variant='ontime' name={id} defaultValue={defaultOptionValue}>
|
<Select
|
||||||
|
placeholder={defaultValue ? undefined : 'Select an option'}
|
||||||
|
variant='ontime'
|
||||||
|
name={id}
|
||||||
|
defaultValue={defaultOptionValue}
|
||||||
|
>
|
||||||
{Object.entries(paramField.values).map(([key, value]) => (
|
{Object.entries(paramField.values).map(([key, value]) => (
|
||||||
<option key={key} value={key}>
|
<option key={key} value={key}>
|
||||||
{value}
|
{value}
|
||||||
@@ -29,19 +35,38 @@ export default function ParamInput({ paramField }: EditFormInputProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'boolean') {
|
if (type === 'boolean') {
|
||||||
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) ?? false;
|
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) || defaultValue;
|
||||||
|
|
||||||
// checked value should be 'true', so it can be captured by the form event
|
// checked value should be 'true', so it can be captured by the form event
|
||||||
return <Switch variant='ontime' name={id} defaultChecked={defaultCheckedValue} value='true' />;
|
return <Switch variant='ontime' name={id} defaultChecked={defaultCheckedValue} value='true' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'number') {
|
if (type === 'number') {
|
||||||
const defaultNumberValue = searchParams.get(id) ?? '';
|
const { prefix, placeholder } = paramField;
|
||||||
|
const defaultNumberValue = searchParams.get(id) ?? defaultValue;
|
||||||
|
|
||||||
return <Input type='number' step='any' variant='ontime-filled' name={id} defaultValue={defaultNumberValue} />;
|
return (
|
||||||
|
<InputGroup variant='ontime-filled'>
|
||||||
|
{prefix && <InputLeftElement pointerEvents='none'>{prefix}</InputLeftElement>}
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
step='any'
|
||||||
|
variant='ontime-filled'
|
||||||
|
name={id}
|
||||||
|
defaultValue={defaultNumberValue}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultStringValue = searchParams.get(id) ?? '';
|
const defaultStringValue = searchParams.get(id) ?? defaultValue;
|
||||||
|
const { prefix, placeholder } = paramField;
|
||||||
|
|
||||||
return <Input variant='ontime-filled' name={id} defaultValue={defaultStringValue} />;
|
return (
|
||||||
|
<InputGroup variant='ontime-filled'>
|
||||||
|
{prefix && <InputLeftElement pointerEvents='none'>{prefix}</InputLeftElement>}
|
||||||
|
<Input name={id} defaultValue={defaultStringValue} placeholder={placeholder} />
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
@use '../../../theme/v2Styles' as *;
|
|
||||||
@use '../../../theme/ontimeColours' as *;
|
|
||||||
|
|
||||||
.drawerContent {
|
.drawerContent {
|
||||||
background-color: $gray-1200;
|
background-color: $gray-1250;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawerHeader {
|
.drawerHeader {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FormEvent, useEffect } from 'react';
|
import { FormEvent, useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Drawer,
|
Drawer,
|
||||||
@@ -12,11 +12,34 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { useLocalStorage } from '../../hooks/useLocalStorage';
|
||||||
|
|
||||||
import ParamInput from './ParamInput';
|
import ParamInput from './ParamInput';
|
||||||
import { ParamField } from './types';
|
import { ParamField } from './types';
|
||||||
|
|
||||||
import style from './ViewParamsEditor.module.scss';
|
import style from './ViewParamsEditor.module.scss';
|
||||||
|
|
||||||
|
type ViewParamsObj = { [key: string]: string | FormDataEntryValue };
|
||||||
|
type SavedViewParams = Record<string, ViewParamsObj>;
|
||||||
|
|
||||||
|
const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj, paramFields: ParamField[]) => {
|
||||||
|
const defaultValues = paramFields.reduce<Record<string, string>>((acc, { id, defaultValue }) => {
|
||||||
|
return { ...acc, [id]: String(defaultValue) };
|
||||||
|
}, {});
|
||||||
|
return Object.entries(paramsObj).reduce((newSearchParams, [id, value]) => {
|
||||||
|
if (typeof value === 'string' && value.length) {
|
||||||
|
if (defaultValues[id] === value) {
|
||||||
|
return newSearchParams;
|
||||||
|
}
|
||||||
|
newSearchParams.set(id, value);
|
||||||
|
|
||||||
|
return newSearchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSearchParams;
|
||||||
|
}, new URLSearchParams());
|
||||||
|
};
|
||||||
|
|
||||||
interface EditFormDrawerProps {
|
interface EditFormDrawerProps {
|
||||||
paramFields: ParamField[];
|
paramFields: ParamField[];
|
||||||
}
|
}
|
||||||
@@ -24,6 +47,8 @@ interface EditFormDrawerProps {
|
|||||||
export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const [storedViewParams, setStoredViewParams] = useLocalStorage<SavedViewParams>('ontime-views', {});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isEditing = searchParams.get('edit');
|
const isEditing = searchParams.get('edit');
|
||||||
@@ -33,36 +58,51 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
}
|
}
|
||||||
}, [searchParams, onOpen]);
|
}, [searchParams, onOpen]);
|
||||||
|
|
||||||
const onEditDrawerClose = () => {
|
/**
|
||||||
|
* disabling this for now, this feature needs more testing
|
||||||
|
* - we seem to have a bug where this is conflicting with the aliases
|
||||||
|
* - I wonder if the logic below needs to be inside an effect,
|
||||||
|
* both localStorage and searchParams should trigger a component update when they change
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const viewParamsObjFromLocalStorage = storedViewParams[pathname];
|
||||||
|
|
||||||
|
if (viewParamsObjFromLocalStorage !== undefined) {
|
||||||
|
const defaultSearchParams = getURLSearchParamsFromObj(viewParamsObjFromLocalStorage);
|
||||||
|
setSearchParams(defaultSearchParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
// linter is asking for `setSearchParams` & `storedViewParams` in the useEffect deps
|
||||||
|
// rule is disabled since adding `setSearchParams` & `storedViewParams` results in unnecessary re-renders
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const onCloseWithoutSaving = () => {
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
searchParams.delete('edit');
|
searchParams.delete('edit');
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearParams = () => {
|
const resetParams = () => {
|
||||||
|
setStoredViewParams({ ...storedViewParams, [pathname]: {} });
|
||||||
setSearchParams();
|
setSearchParams();
|
||||||
onClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||||
formEvent.preventDefault();
|
formEvent.preventDefault();
|
||||||
|
|
||||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||||
const newSearchParams = Object.entries(newParamsObject).reduce((newSearchParams, [id, value]) => {
|
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, paramFields);
|
||||||
if (typeof value === 'string' && value.length) {
|
|
||||||
newSearchParams.set(id, value);
|
|
||||||
|
|
||||||
return newSearchParams;
|
setStoredViewParams({ ...storedViewParams, [pathname]: newParamsObject });
|
||||||
}
|
|
||||||
|
|
||||||
return newSearchParams;
|
|
||||||
}, new URLSearchParams());
|
|
||||||
setSearchParams(newSearchParams);
|
setSearchParams(newSearchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} placement='right' onClose={onEditDrawerClose} size='lg'>
|
<Drawer isOpen={isOpen} placement='right' onClose={onCloseWithoutSaving} size='lg'>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerHeader className={style.drawerHeader}>
|
<DrawerHeader className={style.drawerHeader}>
|
||||||
@@ -85,10 +125,10 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
</DrawerBody>
|
</DrawerBody>
|
||||||
|
|
||||||
<DrawerFooter className={style.drawerFooter}>
|
<DrawerFooter className={style.drawerFooter}>
|
||||||
<Button variant='ontime-ghosted' onClick={clearParams} type='reset'>
|
<Button variant='ontime-ghosted' onClick={resetParams} type='reset'>
|
||||||
Clear
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='ontime-subtle' onClick={onEditDrawerClose}>
|
<Button variant='ontime-subtle' onClick={onCloseWithoutSaving}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
|
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
|
||||||
|
|||||||
@@ -1,44 +1,65 @@
|
|||||||
|
import { UserFields } from 'ontime-types';
|
||||||
|
|
||||||
import { ParamField } from './types';
|
import { ParamField } from './types';
|
||||||
|
|
||||||
export const TIME_FORMAT_OPTION: ParamField = {
|
const getTimeOption = (timeFormat: string): ParamField => {
|
||||||
id: 'format',
|
const placeholder = `${timeFormat} (default)`;
|
||||||
title: '12 / 24 hour timer',
|
return {
|
||||||
description: 'Whether to show the time in 12 or 24 hour mode. Overrides the global setting from preferences',
|
id: 'timeformat',
|
||||||
type: 'option',
|
title: 'Time format string, taken from the Application Settings',
|
||||||
values: { '12': '12 hour AM/PM', '24': '24 hour' },
|
description: 'Format for auxiliar time fields (not the running), eg. HH:mm:ss or hh:mm:ss a, see docs for help',
|
||||||
|
type: 'string',
|
||||||
|
placeholder,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CLOCK_OPTIONS: ParamField[] = [
|
const hideTimerSeconds: ParamField = {
|
||||||
TIME_FORMAT_OPTION,
|
id: 'hideTimerSeconds',
|
||||||
|
title: 'Hide seconds in timer',
|
||||||
|
description: 'Whether to hide seconds in the running timer',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getClockOptions = (timeFormat: string): ParamField[] => [
|
||||||
|
getTimeOption(timeFormat),
|
||||||
{
|
{
|
||||||
id: 'key',
|
id: 'key',
|
||||||
title: 'Key Colour',
|
title: 'Key Colour',
|
||||||
description: 'Background colour in hexadecimal',
|
description: 'Background colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'text',
|
id: 'text',
|
||||||
title: 'Text Colour',
|
title: 'Text Colour',
|
||||||
description: 'Text colour in hexadecimal',
|
description: 'Text colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'fffff (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'textbg',
|
id: 'textbg',
|
||||||
title: 'Text Background',
|
title: 'Text Background',
|
||||||
description: 'Colour of text background in hexadecimal',
|
description: 'Colour of text background in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'font',
|
id: 'font',
|
||||||
title: 'Font',
|
title: 'Font',
|
||||||
description: 'Font family, will use the fonts available in the system',
|
description: 'Font family, will use the fonts available in the system',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'Arial Black (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'size',
|
id: 'size',
|
||||||
title: 'Text Size',
|
title: 'Text Size',
|
||||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '1 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alignx',
|
id: 'alignx',
|
||||||
@@ -46,12 +67,14 @@ export const CLOCK_OPTIONS: ParamField[] = [
|
|||||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||||
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'offsetx',
|
id: 'offsetx',
|
||||||
title: 'Offset Horizontal',
|
title: 'Offset Horizontal',
|
||||||
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '0 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'aligny',
|
id: 'aligny',
|
||||||
@@ -59,47 +82,96 @@ export const CLOCK_OPTIONS: ParamField[] = [
|
|||||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||||
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'offsety',
|
id: 'offsety',
|
||||||
title: 'Offset Vertical',
|
title: 'Offset Vertical',
|
||||||
description: 'Offsets the timer vertical position by a given amount in pixels',
|
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '0 (default)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TIMER_OPTIONS: ParamField[] = [TIME_FORMAT_OPTION];
|
export const getTimerOptions = (timeFormat: string): ParamField[] => [
|
||||||
|
getTimeOption(timeFormat),
|
||||||
|
hideTimerSeconds,
|
||||||
|
{
|
||||||
|
id: 'hideClock',
|
||||||
|
title: 'Hide Time Now',
|
||||||
|
description: 'Hides the Time Now field',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideCards',
|
||||||
|
title: 'Hide Cards',
|
||||||
|
description: 'Hides the Now and Next cards',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideProgress',
|
||||||
|
title: 'Hide progress bar',
|
||||||
|
description: 'Hides the progress bar',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideMessage',
|
||||||
|
title: 'Hide Presenter Message',
|
||||||
|
description: 'Prevents the screen from displaying messages from the presenter',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideExternal',
|
||||||
|
title: 'Hide External',
|
||||||
|
description: 'Prevents the screen from displaying the external field',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||||
|
hideTimerSeconds,
|
||||||
{
|
{
|
||||||
id: 'key',
|
id: 'key',
|
||||||
title: 'Key Colour',
|
title: 'Key Colour',
|
||||||
description: 'Background colour in hexadecimal',
|
description: 'Background colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'text',
|
id: 'text',
|
||||||
title: 'Text Colour',
|
title: 'Text Colour',
|
||||||
description: 'Text colour in hexadecimal',
|
description: 'Text colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'fffff (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'textbg',
|
id: 'textbg',
|
||||||
title: 'Text Background',
|
title: 'Text Background',
|
||||||
description: 'Colour of text background in hexadecimal',
|
description: 'Colour of text background in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'font',
|
id: 'font',
|
||||||
title: 'Font',
|
title: 'Font',
|
||||||
description: 'Font family, will use the fonts available in the system',
|
description: 'Font family, will use the fonts available in the system',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'Arial Black (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'size',
|
id: 'size',
|
||||||
title: 'Text Size',
|
title: 'Text Size',
|
||||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '1 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alignx',
|
id: 'alignx',
|
||||||
@@ -107,12 +179,14 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
|||||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||||
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'offsetx',
|
id: 'offsetx',
|
||||||
title: 'Offset Horizontal',
|
title: 'Offset Horizontal',
|
||||||
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '0 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'aligny',
|
id: 'aligny',
|
||||||
@@ -120,84 +194,274 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
|||||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||||
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'offsety',
|
id: 'offsety',
|
||||||
title: 'Offset Vertical',
|
title: 'Offset Vertical',
|
||||||
description: 'Offsets the timer vertical position by a given amount in pixels',
|
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '0 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideovertime',
|
id: 'hideovertime',
|
||||||
title: 'Hide Overtime',
|
title: 'Hide Overtime',
|
||||||
description: 'Whether to supress overtime styles (red borders and red text)',
|
description: 'Whether to suppress overtime styles (red borders and red text)',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hidemessages',
|
id: 'hidemessages',
|
||||||
title: 'Hide Message Overlay',
|
title: 'Hide Message Overlay',
|
||||||
description: 'Whether to hide the overlay from showing timer screen messages',
|
description: 'Whether to hide the overlay from showing timer screen messages',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideendmessage',
|
id: 'hideendmessage',
|
||||||
title: 'Hide End Message',
|
title: 'Hide End Message',
|
||||||
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
|
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const LOWER_THIRDS_OPTIONS: ParamField[] = [
|
export const LOWER_THIRD_OPTIONS: ParamField[] = [
|
||||||
{
|
{
|
||||||
id: 'preset',
|
id: 'trigger',
|
||||||
title: 'Preset',
|
title: 'Animation Trigger',
|
||||||
description: 'Selects a style preset (0-1)',
|
description: '',
|
||||||
type: 'number',
|
type: 'option',
|
||||||
|
values: {
|
||||||
|
event: 'Event Load',
|
||||||
|
manual: 'Manual',
|
||||||
|
},
|
||||||
|
defaultValue: 'event',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'size',
|
id: 'top-src',
|
||||||
title: 'Size',
|
title: 'Top Text',
|
||||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
description: '',
|
||||||
|
type: 'option',
|
||||||
|
values: {
|
||||||
|
title: 'Title',
|
||||||
|
subtitle: 'Subtitle',
|
||||||
|
presenter: 'Presenter',
|
||||||
|
lowerMsg: 'Lower Thrid Message',
|
||||||
|
},
|
||||||
|
defaultValue: 'title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bottom-src',
|
||||||
|
title: 'Bottom Text',
|
||||||
|
description: 'Select the text source for the bottom element',
|
||||||
|
type: 'option',
|
||||||
|
values: {
|
||||||
|
title: 'Title',
|
||||||
|
subtitle: 'Subtitle',
|
||||||
|
presenter: 'Presenter',
|
||||||
|
lowerMsg: 'Lower Thrid Message',
|
||||||
|
},
|
||||||
|
defaultValue: 'subtitle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'top-colour',
|
||||||
|
title: 'Top Text Colour',
|
||||||
|
description: 'Top text colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '0000ff (default)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bottom-colour',
|
||||||
|
title: 'Bottom Text Colour',
|
||||||
|
description: 'Bottom text colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '0000ff (default)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'top-bg',
|
||||||
|
title: 'Top Background Colour',
|
||||||
|
description: 'Top text background colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bottom-bg',
|
||||||
|
title: 'Bottom Background Colour',
|
||||||
|
description: 'Bottom text background colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'top-size',
|
||||||
|
title: 'Top Text Size',
|
||||||
|
description: 'Font size of the top text',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '65px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bottom-size',
|
||||||
|
title: 'Bottom Text Size',
|
||||||
|
description: 'Font size of the bottom text',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '64px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'width',
|
||||||
|
title: 'Minimum Width',
|
||||||
|
description: 'Minimum Width of the element',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
prefix: '%',
|
||||||
|
placeholder: '45 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'transition',
|
id: 'transition',
|
||||||
title: 'Transition',
|
title: 'Transition',
|
||||||
description: 'Transition in time in seconds (default 5)',
|
description: 'Transition in time in seconds (default 3)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '3 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'text',
|
id: 'delay',
|
||||||
title: 'Text Colour',
|
title: 'Delay',
|
||||||
description: 'Text colour in hexadecimal',
|
description: 'Delay between transition in and out in seconds (default 3)',
|
||||||
type: 'string',
|
type: 'number',
|
||||||
},
|
placeholder: '3 (default)',
|
||||||
{
|
|
||||||
id: 'bg',
|
|
||||||
title: 'Text Background',
|
|
||||||
description: 'Text background colour in hexadecimal',
|
|
||||||
type: 'string',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'key',
|
id: 'key',
|
||||||
title: 'Key Colour',
|
title: 'Key Colour',
|
||||||
description: 'Screen background colour in hexadecimal',
|
description: 'Colour of the background',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'ffffffff (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fadeout',
|
id: 'line-colour',
|
||||||
title: 'Fadeout',
|
title: 'Line Colour',
|
||||||
description: 'Time (in seconds) the lower third displays before fading out',
|
description: 'Colour of the line',
|
||||||
type: 'number',
|
prefix: '#',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'ff0000ff (default)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
|
export const getBackstageOptions = (timeFormat: string): ParamField[] => [
|
||||||
TIME_FORMAT_OPTION,
|
getTimeOption(timeFormat),
|
||||||
{
|
{
|
||||||
id: 'seconds',
|
id: 'hidePast',
|
||||||
title: 'Show Seconds',
|
title: 'Hide past events',
|
||||||
description: 'Shows seconds in clock',
|
description: 'Scheduler will only show upcoming events',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stopCycle',
|
||||||
|
title: 'Stop cycling through event pages',
|
||||||
|
description: 'Schedule will not auto-cycle through events',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'eventsPerPage',
|
||||||
|
title: 'Events per page',
|
||||||
|
description: 'Sets the number of events on the page, can cause overlow',
|
||||||
|
type: 'number',
|
||||||
|
placeholder: '7 (default)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const getPublicOptions = (timeFormat: string): ParamField[] => [
|
||||||
|
getTimeOption(timeFormat),
|
||||||
|
{
|
||||||
|
id: 'hidePast',
|
||||||
|
title: 'Hide past events',
|
||||||
|
description: 'Scheduler will only show upcoming events',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stopCycle',
|
||||||
|
title: 'Stop cycling through event pages',
|
||||||
|
description: 'Schedule will not auto-cycle through events',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'eventsPerPage',
|
||||||
|
title: 'Events per page',
|
||||||
|
description: 'Sets the number of events on the page, can cause overlow',
|
||||||
|
type: 'number',
|
||||||
|
placeholder: '7 (default)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const getStudioClockOptions = (timeFormat: string): ParamField[] => [
|
||||||
|
getTimeOption(timeFormat),
|
||||||
|
hideTimerSeconds,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getOperatorOptions = (userFields: UserFields, timeFormat: string): ParamField[] => {
|
||||||
|
return [
|
||||||
|
getTimeOption(timeFormat),
|
||||||
|
{
|
||||||
|
id: 'hidepast',
|
||||||
|
title: 'Hide Past Events',
|
||||||
|
description: 'Whether to events that have passed',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'main',
|
||||||
|
title: 'Main data field',
|
||||||
|
description: 'Field to be shown in the first line of text',
|
||||||
|
type: 'option',
|
||||||
|
values: {
|
||||||
|
title: 'Title',
|
||||||
|
subtitle: 'Subtitle',
|
||||||
|
presenter: 'Presenter',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'secondary',
|
||||||
|
title: 'Secondary data field',
|
||||||
|
description: 'Field to be shown in the second line of text',
|
||||||
|
type: 'option',
|
||||||
|
values: {
|
||||||
|
title: 'Title',
|
||||||
|
subtitle: 'Subtitle',
|
||||||
|
presenter: 'Presenter',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subscribe',
|
||||||
|
title: 'Highlight Field',
|
||||||
|
description: 'Choose a field to highlight',
|
||||||
|
type: 'option',
|
||||||
|
values: {
|
||||||
|
user0: userFields.user0 || 'user0',
|
||||||
|
user1: userFields.user1 || 'user1',
|
||||||
|
user2: userFields.user2 || 'user2',
|
||||||
|
user3: userFields.user3 || 'user3',
|
||||||
|
user4: userFields.user4 || 'user4',
|
||||||
|
user5: userFields.user5 || 'user5',
|
||||||
|
user6: userFields.user6 || 'user6',
|
||||||
|
user7: userFields.user7 || 'user7',
|
||||||
|
user8: userFields.user8 || 'user8',
|
||||||
|
user9: userFields.user9 || 'user9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shouldEdit',
|
||||||
|
title: 'Edit user field',
|
||||||
|
description: 'Allows editing an events user field by long pressing on it. Needs a selected highlighted field',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCountdownOptions = (timeFormat: string): ParamField[] => [getTimeOption(timeFormat), hideTimerSeconds];
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ type BaseField = {
|
|||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OptionsField = { type: 'option'; values: Record<string, string> };
|
type OptionsField = {
|
||||||
type StringField = { type: 'string' };
|
type: 'option';
|
||||||
type BooleanField = { type: 'boolean' };
|
values: Record<string, string>;
|
||||||
type NumberField = { type: 'number' };
|
defaultValue?: string;
|
||||||
|
};
|
||||||
|
type StringField = { type: 'string'; defaultValue?: string; prefix?: string; placeholder?: string };
|
||||||
|
type NumberField = { type: 'number'; defaultValue?: number; prefix?: string; placeholder?: string };
|
||||||
|
type BooleanField = { type: 'boolean'; defaultValue: boolean };
|
||||||
|
|
||||||
export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField);
|
export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
|
|||||||
const [operatorAuth, setOperatorAuth] = useState(true);
|
const [operatorAuth, setOperatorAuth] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'loading') return;
|
if (status === 'pending') return;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const previousEditor = sessionStorage.getItem(storageKeys.editor);
|
const previousEditor = sessionStorage.getItem(storageKeys.editor);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// roughly from https://github.com/juliencrn/usehooks-ts/blob/master/packages/usehooks-ts/src/useMediaQuery/useMediaQuery.ts
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function getMatches(query: string): boolean {
|
||||||
|
return window.matchMedia(query).matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: debounce handleChange
|
||||||
|
export default function useMediaQuery(query: string): boolean {
|
||||||
|
const [matches, setMatches] = useState<boolean>(getMatches(query));
|
||||||
|
|
||||||
|
const handleChange = useCallback(() => {
|
||||||
|
setMatches(getMatches(query));
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const matchMedia = window.matchMedia(query);
|
||||||
|
|
||||||
|
// Triggered at the first client-side load and if query changes
|
||||||
|
handleChange();
|
||||||
|
|
||||||
|
// Listen matchMedia
|
||||||
|
matchMedia.addEventListener('change', handleChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
matchMedia.removeEventListener('change', handleChange);
|
||||||
|
};
|
||||||
|
}, [handleChange, query]);
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|
||||||
import { EVENT_DATA } from '../api/apiConstants';
|
|
||||||
import { fetchEventData } from '../api/eventDataApi';
|
|
||||||
import { eventDataPlaceholder } from '../models/EventData';
|
|
||||||
|
|
||||||
export default function useEventData() {
|
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
|
||||||
queryKey: EVENT_DATA,
|
|
||||||
queryFn: fetchEventData,
|
|
||||||
placeholderData: eventDataPlaceholder,
|
|
||||||
retry: 5,
|
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
|
||||||
networkMode: 'always',
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data, status, isFetching, isError, refetch };
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
|
import { HTTP_SETTINGS } from '../api/apiConstants';
|
||||||
|
import { logAxiosError } from '../api/apiUtils';
|
||||||
|
import { getHTTP, postHTTP } from '../api/ontimeApi';
|
||||||
|
import { httpPlaceholder } from '../models/Http';
|
||||||
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
|
export function useHttpSettings() {
|
||||||
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
|
queryKey: HTTP_SETTINGS,
|
||||||
|
queryFn: getHTTP,
|
||||||
|
placeholderData: httpPlaceholder,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt: number) => attempt * 2500,
|
||||||
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
networkMode: 'always',
|
||||||
|
});
|
||||||
|
|
||||||
|
// we need to jump through some hoops because of the type op port
|
||||||
|
return { data: data ?? httpPlaceholder, status, isFetching, isError, refetch };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePostHttpSettings() {
|
||||||
|
const { isPending, mutateAsync } = useMutation({
|
||||||
|
mutationFn: postHTTP,
|
||||||
|
onError: (error) => logAxiosError('Error saving HTTP settings', error),
|
||||||
|
onSuccess: (res) => {
|
||||||
|
ontimeQueryClient.setQueryData(HTTP_SETTINGS, res.data);
|
||||||
|
},
|
||||||
|
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: HTTP_SETTINGS }),
|
||||||
|
});
|
||||||
|
return { isPending, mutateAsync };
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { GetInfo } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { APP_INFO } from '../api/apiConstants';
|
import { APP_INFO } from '../api/apiConstants';
|
||||||
@@ -6,7 +7,7 @@ import { getInfo } from '../api/ontimeApi';
|
|||||||
import { ontimePlaceholderInfo } from '../models/Info';
|
import { ontimePlaceholderInfo } from '../models/Info';
|
||||||
|
|
||||||
export default function useInfo() {
|
export default function useInfo() {
|
||||||
const { data, status, isError, refetch } = useQuery({
|
const { data, status, isError, refetch, isFetching } = useQuery<GetInfo>({
|
||||||
queryKey: APP_INFO,
|
queryKey: APP_INFO,
|
||||||
queryFn: getInfo,
|
queryFn: getInfo,
|
||||||
placeholderData: ontimePlaceholderInfo,
|
placeholderData: ontimePlaceholderInfo,
|
||||||
@@ -16,5 +17,5 @@ export default function useInfo() {
|
|||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data, status, isError, refetch };
|
return { data, status, isError, refetch, isFetching };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { OSCSettings } from 'ontime-types';
|
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { OSC_SETTINGS } from '../api/apiConstants';
|
import { OSC_SETTINGS } from '../api/apiConstants';
|
||||||
import { logAxiosError } from '../api/apiUtils';
|
import { logAxiosError } from '../api/apiUtils';
|
||||||
import { getOSC, postOSC, postOscSubscriptions } from '../api/ontimeApi';
|
import { getOSC, postOSC } from '../api/ontimeApi';
|
||||||
import { oscPlaceholderSettings } from '../models/OscSettings';
|
import { oscPlaceholderSettings } from '../models/OscSettings';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
@@ -19,25 +18,17 @@ export default function useOscSettings() {
|
|||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
|
||||||
// we need to jump through some hoops because of the type op port
|
return { data: data ?? oscPlaceholderSettings, status, isFetching, isError, refetch };
|
||||||
return { data: data! as unknown as OSCSettings, status, isFetching, isError, refetch };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOscSettingsMutation() {
|
export function useOscSettingsMutation() {
|
||||||
const { isLoading, mutateAsync } = useMutation({
|
const { isPending, mutateAsync } = useMutation({
|
||||||
mutationFn: postOSC,
|
mutationFn: postOSC,
|
||||||
onError: (error) => logAxiosError('Error saving OSC settings', error),
|
onError: (error) => logAxiosError('Error saving OSC settings', error),
|
||||||
onSuccess: (res) => ontimeQueryClient.setQueryData(OSC_SETTINGS, res.data),
|
onSuccess: (res) => {
|
||||||
|
ontimeQueryClient.setQueryData(OSC_SETTINGS, res.data);
|
||||||
|
},
|
||||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
||||||
});
|
});
|
||||||
return { isLoading, mutateAsync };
|
return { isPending, mutateAsync };
|
||||||
}
|
|
||||||
|
|
||||||
export function usePostOscSubscriptions() {
|
|
||||||
const { isLoading, mutateAsync } = useMutation({
|
|
||||||
mutationFn: postOscSubscriptions,
|
|
||||||
onError: (error) => logAxiosError('Error saving OSC settings', error),
|
|
||||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
|
||||||
});
|
|
||||||
return { isLoading, mutateAsync };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
|
import { PROJECT_DATA } from '../api/apiConstants';
|
||||||
|
import { getProjectData } from '../api/projectDataApi';
|
||||||
|
import { projectDataPlaceholder } from '../models/ProjectData';
|
||||||
|
|
||||||
|
export default function useProjectData() {
|
||||||
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
|
queryKey: PROJECT_DATA,
|
||||||
|
queryFn: getProjectData,
|
||||||
|
placeholderData: projectDataPlaceholder,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
networkMode: 'always',
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data: data ?? projectDataPlaceholder, status, isFetching, isError, refetch };
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { ProjectFileListResponse } from 'ontime-types';
|
||||||
|
|
||||||
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
|
import { PROJECT_LIST } from '../api/apiConstants';
|
||||||
|
import { getProjects } from '../api/ontimeApi';
|
||||||
|
|
||||||
|
const placeholderProjectList: ProjectFileListResponse = {
|
||||||
|
files: [],
|
||||||
|
lastLoadedProject: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useProjectList() {
|
||||||
|
const { data, status, refetch } = useQuery({
|
||||||
|
queryKey: PROJECT_LIST,
|
||||||
|
queryFn: getProjects,
|
||||||
|
placeholderData: placeholderProjectList,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt: number) => attempt * 2500,
|
||||||
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
networkMode: 'always',
|
||||||
|
});
|
||||||
|
return { data: data ?? placeholderProjectList, status, refetch };
|
||||||
|
}
|
||||||
@@ -1,19 +1,41 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { NormalisedRundown, OntimeRundown, RundownCached } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchInterval } from '../../ontimeConfig';
|
import { queryRefetchInterval } from '../../ontimeConfig';
|
||||||
import { RUNDOWN_TABLE } from '../api/apiConstants';
|
import { RUNDOWN } from '../api/apiConstants';
|
||||||
import { fetchRundown } from '../api/eventsApi';
|
import { fetchCachedRundown } from '../api/eventsApi';
|
||||||
|
|
||||||
|
// revision is -1 so that the remote revision is higher
|
||||||
|
const cachedRundownPlaceholder = { order: [] as string[], rundown: {} as NormalisedRundown, revision: -1 };
|
||||||
|
|
||||||
export default function useRundown() {
|
export default function useRundown() {
|
||||||
const { data, status, isError, refetch } = useQuery({
|
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
||||||
queryKey: RUNDOWN_TABLE,
|
queryKey: RUNDOWN,
|
||||||
queryFn: fetchRundown,
|
queryFn: fetchCachedRundown,
|
||||||
placeholderData: [],
|
placeholderData: cachedRundownPlaceholder,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchInterval,
|
refetchInterval: queryRefetchInterval,
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
||||||
return { data, status, isError, refetch };
|
}
|
||||||
|
|
||||||
|
export function useFlatRundown() {
|
||||||
|
const { data, status } = useRundown();
|
||||||
|
|
||||||
|
const [prevRevision, setPrevRevision] = useState<number>(-1);
|
||||||
|
const [flatRunDown, setFlatRunDown] = useState<OntimeRundown>([]);
|
||||||
|
|
||||||
|
// update data whenever the revision changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.revision !== -1 && data.revision !== prevRevision) {
|
||||||
|
const flatRundown = data.order.map((id) => data.rundown[id]);
|
||||||
|
setFlatRunDown(flatRundown);
|
||||||
|
setPrevRevision(data.revision);
|
||||||
|
}
|
||||||
|
}, [data.order, data.revision, data.rundown, prevRevision]);
|
||||||
|
|
||||||
|
return { data: flatRunDown, status };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export default function useClickOutside<T extends HTMLElement = HTMLElement>(
|
|||||||
ref: RefObject<T>,
|
ref: RefObject<T>,
|
||||||
callback: ClickOutsideEventHandler,
|
callback: ClickOutsideEventHandler,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClick(event: MouseEvent) {
|
function handleClick(event: MouseEvent) {
|
||||||
const element = ref?.current;
|
const element = ref?.current;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export default function useElectronEvent() {
|
export default function useElectronEvent() {
|
||||||
const isElectron = window?.process?.type === 'renderer';
|
const isElectron = window?.process?.type === 'renderer';
|
||||||
|
|
||||||
const sendToElectron = (channel: string, args?: string | Record<string, any>) => {
|
const sendToElectron = (channel: string, args?: string | Record<string, unknown>) => {
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
window?.ipcRenderer.send(channel, args);
|
window?.ipcRenderer.send(channel, args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||||
import { getCueCandidate, swapOntimeEvents } from 'ontime-utils';
|
import { getLinkedTimes, getPreviousEventNormal, reorderArray, swapEventData } from 'ontime-utils';
|
||||||
|
|
||||||
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
import { RUNDOWN } from '../api/apiConstants';
|
||||||
import { logAxiosError } from '../api/apiUtils';
|
import { logAxiosError } from '../api/apiUtils';
|
||||||
import {
|
import {
|
||||||
ReorderEntry,
|
ReorderEntry,
|
||||||
requestApplyDelay,
|
requestApplyDelay,
|
||||||
|
requestBatchPutEvents,
|
||||||
requestDelete,
|
requestDelete,
|
||||||
requestDeleteAll,
|
requestDeleteAll,
|
||||||
requestEventSwap,
|
requestEventSwap,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
SwapEntry,
|
SwapEntry,
|
||||||
} from '../api/eventsApi';
|
} from '../api/eventsApi';
|
||||||
import { useEditorSettings } from '../stores/editorSettings';
|
import { useEditorSettings } from '../stores/editorSettings';
|
||||||
|
import { forgivingStringToMillis } from '../utils/dateConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Set of utilities for events
|
* @description Set of utilities for events
|
||||||
@@ -32,11 +34,9 @@ export const useEventAction = () => {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const _addEventMutation = useMutation({
|
const _addEventMutation = useMutation({
|
||||||
// Mutation finished, failed or successful
|
|
||||||
// Fetch anyway, just to be sure
|
|
||||||
mutationFn: requestPostEvent,
|
mutationFn: requestPostEvent,
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -67,20 +67,13 @@ export const useEventAction = () => {
|
|||||||
after: options?.after,
|
after: options?.after,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (newEvent?.cue === undefined) {
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this has a value
|
||||||
newEvent.cue = getCueCandidate(queryClient.getQueryData(RUNDOWN_TABLE) || [], options?.after);
|
const rundownData = queryClient.getQueryData<RundownCached>(RUNDOWN)!;
|
||||||
}
|
const { rundown } = rundownData;
|
||||||
|
|
||||||
// hard coding duration value to be as expected for now
|
|
||||||
// this until timeOptions gets implemented
|
|
||||||
if (newEvent?.timeStart !== undefined && newEvent.timeEnd !== undefined) {
|
|
||||||
newEvent.duration = Math.max(0, newEvent?.timeEnd - newEvent?.timeStart) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
|
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
|
||||||
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
|
const previousEvent = rundown[applicationOptions.lastEventId];
|
||||||
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
|
if (isOntimeEvent(previousEvent)) {
|
||||||
if (previousEvent !== undefined && previousEvent.type === 'event') {
|
|
||||||
newEvent.timeStart = previousEvent.timeEnd;
|
newEvent.timeStart = previousEvent.timeEnd;
|
||||||
newEvent.timeEnd = previousEvent.timeEnd;
|
newEvent.timeEnd = previousEvent.timeEnd;
|
||||||
}
|
}
|
||||||
@@ -97,10 +90,9 @@ export const useEventAction = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error -- we know that the object is well formed now
|
|
||||||
await _addEventMutation.mutateAsync(newEvent);
|
await _addEventMutation.mutateAsync(newEvent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error fetching data', error);
|
logAxiosError('Failed adding event', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[_addEventMutation, defaultPublic, queryClient, startTimeIsLastEnd],
|
[_addEventMutation, defaultPublic, queryClient, startTimeIsLastEnd],
|
||||||
@@ -115,25 +107,31 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (newEvent) => {
|
onMutate: async (newEvent) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries([RUNDOWN_TABLE_KEY, newEvent.id]);
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvent = queryClient.getQueryData([RUNDOWN_TABLE_KEY, newEvent.id]);
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
const eventId = newEvent.id;
|
||||||
|
|
||||||
// optimistically update object
|
if (previousData && eventId) {
|
||||||
queryClient.setQueryData([RUNDOWN_TABLE_KEY, newEvent.id], newEvent);
|
// optimistically update object
|
||||||
|
const newRundown = { ...previousData.rundown };
|
||||||
|
// @ts-expect-error -- we expect the events to be of same type
|
||||||
|
newRundown[eventId] = { ...newRundown[eventId], ...newEvent };
|
||||||
|
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
||||||
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvent, newEvent };
|
return { previousData, newEvent };
|
||||||
},
|
},
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _newEvent, context) => {
|
onError: (_error, _newEvent, context) => {
|
||||||
queryClient.setQueryData([RUNDOWN_TABLE_KEY, context?.newEvent.id], context?.previousEvent);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: async () => {
|
onSettled: async () => {
|
||||||
await queryClient.invalidateQueries([RUNDOWN_TABLE_KEY]);
|
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -152,6 +150,154 @@ export const useEventAction = () => {
|
|||||||
[_updateEventMutation],
|
[_updateEventMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type TimeField = 'timeStart' | 'timeEnd' | 'duration';
|
||||||
|
/**
|
||||||
|
* Updates time of existing event
|
||||||
|
*/
|
||||||
|
const updateTimer = useCallback(
|
||||||
|
async (eventId: string, field: TimeField, value: string) => {
|
||||||
|
const getPreviousEnd = (): number => {
|
||||||
|
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
|
if (!cachedRundown?.order || !cachedRundown?.rundown) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = cachedRundown.order.indexOf(eventId);
|
||||||
|
if (index === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let previousEnd = 0;
|
||||||
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
|
const event = cachedRundown.rundown[cachedRundown.order[i]];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
previousEnd = event.timeEnd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return previousEnd;
|
||||||
|
};
|
||||||
|
|
||||||
|
let newValMillis = 0;
|
||||||
|
|
||||||
|
// check for previous keyword
|
||||||
|
if (value === 'p' || value === 'prev' || value === 'previous') {
|
||||||
|
newValMillis = getPreviousEnd();
|
||||||
|
|
||||||
|
// check for adding time keyword
|
||||||
|
} else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) {
|
||||||
|
// TODO: is this logic solid?
|
||||||
|
const remainingString = value.substring(1);
|
||||||
|
newValMillis = getPreviousEnd() + forgivingStringToMillis(remainingString);
|
||||||
|
} else {
|
||||||
|
newValMillis = forgivingStringToMillis(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEvent = {
|
||||||
|
id: eventId,
|
||||||
|
[field]: newValMillis,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await _updateEventMutation.mutateAsync(newEvent);
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosError('Error updating event', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[_updateEventMutation, queryClient],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles link of an event to the previous
|
||||||
|
*/
|
||||||
|
const linkTimer = useCallback(
|
||||||
|
async (eventId: string, linkStart: MaybeString) => {
|
||||||
|
let newEvent: Partial<OntimeEvent> = { id: eventId };
|
||||||
|
|
||||||
|
if (!linkStart) {
|
||||||
|
newEvent.linkStart = null;
|
||||||
|
} else {
|
||||||
|
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
if (!cachedRundown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentEvent = cachedRundown.rundown[eventId] as OntimeEvent;
|
||||||
|
if (!isOntimeEvent(currentEvent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { previousEvent } = getPreviousEventNormal(cachedRundown.rundown, cachedRundown.order, eventId);
|
||||||
|
|
||||||
|
if (!previousEvent) {
|
||||||
|
newEvent.linkStart = null;
|
||||||
|
} else {
|
||||||
|
newEvent.linkStart = previousEvent.id;
|
||||||
|
const timePatch = getLinkedTimes(currentEvent, previousEvent);
|
||||||
|
newEvent = { ...newEvent, ...timePatch };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _updateEventMutation.mutateAsync(newEvent);
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosError('Error updating event', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[_updateEventMutation, queryClient],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls mutation to edit multiple events
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const _batchUpdateEventsMutation = useMutation({
|
||||||
|
mutationFn: requestBatchPutEvents,
|
||||||
|
onMutate: async ({ ids, data }) => {
|
||||||
|
// cancel ongoing queries
|
||||||
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
|
// Snapshot the previous value
|
||||||
|
const previousEvents = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
|
if (previousEvents) {
|
||||||
|
const eventIds = new Set(ids);
|
||||||
|
const newRundown = { ...previousEvents.rundown };
|
||||||
|
|
||||||
|
eventIds.forEach((eventId) => {
|
||||||
|
if (Object.hasOwn(newRundown, eventId)) {
|
||||||
|
const event = newRundown[eventId];
|
||||||
|
if (isOntimeEvent(event)) {
|
||||||
|
newRundown[eventId] = {
|
||||||
|
...event,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.setQueryData(RUNDOWN, { order: previousEvents.order, rundown: newRundown, revision: -1 });
|
||||||
|
}
|
||||||
|
// Return a context with the previous and new events
|
||||||
|
return { previousEvents };
|
||||||
|
},
|
||||||
|
onSettled: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
|
},
|
||||||
|
onError: (_error, _newEvent, context) => {
|
||||||
|
queryClient.setQueryData(RUNDOWN, context?.previousEvents);
|
||||||
|
},
|
||||||
|
networkMode: 'always',
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchUpdateEvents = useCallback(
|
||||||
|
async (data: Partial<OntimeEvent>, eventIds: string[]) => {
|
||||||
|
try {
|
||||||
|
await _batchUpdateEventsMutation.mutateAsync({ ids: eventIds, data });
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosError('Error updating events', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[_batchUpdateEventsMutation],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to delete an event
|
* Calls mutation to delete an event
|
||||||
* @private
|
* @private
|
||||||
@@ -161,28 +307,36 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (eventId) => {
|
onMutate: async (eventId) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries([RUNDOWN_TABLE_KEY, eventId]);
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
const filtered = [...(previousEvents as OntimeRundown)].filter((e) => e.id !== eventId);
|
if (previousData) {
|
||||||
|
// optimistically update object
|
||||||
|
const newOrder = previousData.order.filter((id) => id !== eventId);
|
||||||
|
const newRundown = { ...previousData.rundown };
|
||||||
|
delete newRundown[eventId];
|
||||||
|
|
||||||
// optimistically update object
|
queryClient.setQueryData(RUNDOWN, {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, filtered);
|
order: newOrder,
|
||||||
|
rundown: newRundown,
|
||||||
|
revision: -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvents };
|
return { previousData };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -210,26 +364,26 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async () => {
|
onMutate: async () => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, []);
|
queryClient.setQueryData(RUNDOWN, { rundown: {}, order: [], revision: -1 });
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvents };
|
return { previousData };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation fails, rollback undos optimist update
|
// Mutation fails, rollback undos optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -253,7 +407,7 @@ export const useEventAction = () => {
|
|||||||
mutationFn: requestApplyDelay,
|
mutationFn: requestApplyDelay,
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -281,30 +435,30 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (data) => {
|
onMutate: async (data) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
|
||||||
const e = [...(previousEvents as OntimeRundown)];
|
if (previousData) {
|
||||||
const [reorderedItem] = e.splice(data.from, 1);
|
// optimistically update object
|
||||||
e.splice(data.to, 0, reorderedItem);
|
const newOrder = reorderArray(previousData.order, data.from, data.to);
|
||||||
|
|
||||||
// optimistically update object
|
queryClient.setQueryData(RUNDOWN, { order: newOrder, rundown: previousData.rundown, revision: -1 });
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, e);
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvents };
|
return { previousData };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -316,9 +470,9 @@ export const useEventAction = () => {
|
|||||||
async (eventId: string, from: number, to: number) => {
|
async (eventId: string, from: number, to: number) => {
|
||||||
try {
|
try {
|
||||||
const reorderObject: ReorderEntry = {
|
const reorderObject: ReorderEntry = {
|
||||||
eventId: eventId,
|
eventId,
|
||||||
from: from,
|
from,
|
||||||
to: to,
|
to,
|
||||||
};
|
};
|
||||||
await _reorderEventMutation.mutateAsync(reorderObject);
|
await _reorderEventMutation.mutateAsync(reorderObject);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -337,31 +491,39 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async ({ from, to }) => {
|
onMutate: async ({ from, to }) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
|
const previousData = queryClient.getQueryData<RundownCached>(RUNDOWN);
|
||||||
|
if (previousData) {
|
||||||
|
// optimistically update object
|
||||||
|
const newRundown = { ...previousData.rundown };
|
||||||
|
const eventA = previousData.rundown[from];
|
||||||
|
const eventB = previousData.rundown[to];
|
||||||
|
|
||||||
const fromEventIndex = rundown.findIndex((event) => event.id === from);
|
if (!isOntimeEvent(eventA) || !isOntimeEvent(eventB)) {
|
||||||
const toEventIndex = rundown.findIndex((event) => event.id === to);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const previousEvents = swapOntimeEvents(rundown, fromEventIndex, toEventIndex);
|
const { newA, newB } = swapEventData(eventA, eventB);
|
||||||
|
newRundown[from] = newA;
|
||||||
|
newRundown[to] = newB;
|
||||||
|
|
||||||
// optimistically update object
|
queryClient.setQueryData(RUNDOWN, { order: previousData.order, rundown: newRundown, revision: -1 });
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, previousEvents);
|
}
|
||||||
|
|
||||||
// Return a context with the previous events
|
// Return a context with the previous events
|
||||||
return { previousEvents };
|
return { previousData };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -371,8 +533,6 @@ export const useEventAction = () => {
|
|||||||
*/
|
*/
|
||||||
const swapEvents = useCallback(
|
const swapEvents = useCallback(
|
||||||
async ({ from, to }: SwapEntry) => {
|
async ({ from, to }: SwapEntry) => {
|
||||||
// TODO: before calling `/swapEvents`,
|
|
||||||
// we should determine the events are of type `OntimeEvent`
|
|
||||||
try {
|
try {
|
||||||
await _swapEvents.mutateAsync({ from, to });
|
await _swapEvents.mutateAsync({ from, to });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -384,11 +544,14 @@ export const useEventAction = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
addEvent,
|
addEvent,
|
||||||
updateEvent,
|
applyDelay,
|
||||||
|
batchUpdateEvents,
|
||||||
deleteEvent,
|
deleteEvent,
|
||||||
deleteAllEvents,
|
deleteAllEvents,
|
||||||
applyDelay,
|
linkTimer,
|
||||||
reorderEvent,
|
reorderEvent,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
|
updateEvent,
|
||||||
|
updateTimer,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
export type TLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
export type TLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
||||||
|
|
||||||
@@ -26,13 +20,13 @@ const LOG_LEVEL: Record<TLogLevel, number> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useFitText = ({
|
const useFitText = ({
|
||||||
logLevel: logLevelOption = 'info',
|
logLevel: logLevelOption = 'info',
|
||||||
maxFontSize = 100,
|
maxFontSize = 100,
|
||||||
minFontSize = 20,
|
minFontSize = 20,
|
||||||
onFinish,
|
onFinish,
|
||||||
onStart,
|
onStart,
|
||||||
resolution = 5,
|
resolution = 5,
|
||||||
}: TOptions = {}) => {
|
}: TOptions = {}) => {
|
||||||
const logLevel = LOG_LEVEL[logLevelOption];
|
const logLevel = LOG_LEVEL[logLevelOption];
|
||||||
|
|
||||||
const initState = useCallback(() => {
|
const initState = useCallback(() => {
|
||||||
@@ -112,8 +106,7 @@ const useFitText = ({
|
|||||||
const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;
|
const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;
|
||||||
const isOverflow =
|
const isOverflow =
|
||||||
!!ref.current &&
|
!!ref.current &&
|
||||||
(ref.current.scrollHeight > ref.current.offsetHeight ||
|
(ref.current.scrollHeight > ref.current.offsetHeight || ref.current.scrollWidth > ref.current.offsetWidth);
|
||||||
ref.current.scrollWidth > ref.current.offsetWidth);
|
|
||||||
const isFailed = isOverflow && fontSize === fontSizePrev;
|
const isFailed = isOverflow && fontSize === fontSizePrev;
|
||||||
const isAsc = fontSize > fontSizePrev;
|
const isAsc = fontSize > fontSizePrev;
|
||||||
|
|
||||||
@@ -123,9 +116,7 @@ const useFitText = ({
|
|||||||
if (isFailed) {
|
if (isFailed) {
|
||||||
isCalculatingRef.current = false;
|
isCalculatingRef.current = false;
|
||||||
if (logLevel <= LOG_LEVEL.info) {
|
if (logLevel <= LOG_LEVEL.info) {
|
||||||
console.info(
|
console.info(`[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`);
|
||||||
`[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (isOverflow) {
|
} else if (isOverflow) {
|
||||||
setState({
|
setState({
|
||||||
@@ -160,16 +151,7 @@ const useFitText = ({
|
|||||||
fontSizeMin: newMin,
|
fontSizeMin: newMin,
|
||||||
fontSizePrev: fontSize,
|
fontSizePrev: fontSize,
|
||||||
});
|
});
|
||||||
}, [
|
}, [calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev, onFinish, ref, resolution]);
|
||||||
calcKey,
|
|
||||||
fontSize,
|
|
||||||
fontSizeMax,
|
|
||||||
fontSizeMin,
|
|
||||||
fontSizePrev,
|
|
||||||
onFinish,
|
|
||||||
ref,
|
|
||||||
resolution,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { fontSize: `${fontSize}%`, ref };
|
return { fontSize: `${fontSize}%`, ref };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { MutableRefObject, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
|
||||||
|
componentRef: MutableRefObject<ComponentRef>,
|
||||||
|
scrollRef: MutableRefObject<ScrollRef>,
|
||||||
|
topOffset: number,
|
||||||
|
) {
|
||||||
|
if (!componentRef.current || !scrollRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentRect = componentRef.current.getBoundingClientRect();
|
||||||
|
const scrollRect = scrollRef.current.getBoundingClientRect();
|
||||||
|
const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - topOffset;
|
||||||
|
|
||||||
|
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseFollowComponentProps {
|
||||||
|
followRef: MutableRefObject<HTMLElement | null>;
|
||||||
|
scrollRef: MutableRefObject<HTMLElement | null>;
|
||||||
|
doFollow: boolean;
|
||||||
|
topOffset?: number;
|
||||||
|
setScrollFlag?: (newValue: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useFollowComponent(props: UseFollowComponentProps) {
|
||||||
|
const { followRef, scrollRef, doFollow, topOffset = 100, setScrollFlag } = props;
|
||||||
|
|
||||||
|
// when cursor moves, view should follow
|
||||||
|
useEffect(() => {
|
||||||
|
if (!doFollow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (followRef.current && scrollRef.current) {
|
||||||
|
setScrollFlag?.(true);
|
||||||
|
// Use requestAnimationFrame to ensure the component is fully loaded
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
scrollToComponent(
|
||||||
|
followRef as MutableRefObject<HTMLElement>,
|
||||||
|
scrollRef as MutableRefObject<HTMLElement>,
|
||||||
|
topOffset,
|
||||||
|
);
|
||||||
|
setScrollFlag?.(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line -- the prompt seems incorrect
|
||||||
|
}, [followRef?.current, scrollRef?.current]);
|
||||||
|
|
||||||
|
const scrollToRefComponent = useCallback(
|
||||||
|
(componentRef = followRef, containerRef = scrollRef, offset = topOffset) => {
|
||||||
|
if (componentRef.current && containerRef.current) {
|
||||||
|
// @ts-expect-error -- we know this are not null
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
scrollToComponent(componentRef!, scrollRef!, offset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[followRef, scrollRef, topOffset],
|
||||||
|
);
|
||||||
|
|
||||||
|
return scrollToRefComponent;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-nocheck -- working on it
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface WebkitDocument extends Document {
|
interface WebkitDocument extends Document {
|
||||||
@@ -42,7 +44,7 @@ export default function useFullscreen() {
|
|||||||
});
|
});
|
||||||
} else if (element.webkitRequestFullscreen) {
|
} else if (element.webkitRequestFullscreen) {
|
||||||
// iOS Safari fullscreen API is supported
|
// iOS Safari fullscreen API is supported
|
||||||
element.webkitRequestFullscreen().catch(() => {
|
element.webkitRequestFullscreen?.().catch(() => {
|
||||||
/* nothing to do */
|
/* nothing to do */
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -55,7 +57,7 @@ export default function useFullscreen() {
|
|||||||
});
|
});
|
||||||
} else if ((document as WebkitDocument).webkitExitFullscreen) {
|
} else if ((document as WebkitDocument).webkitExitFullscreen) {
|
||||||
// iOS Safari fullscreen API is supported
|
// iOS Safari fullscreen API is supported
|
||||||
(document as WebkitDocument).webkitExitFullscreen().catch(() => {
|
(document as WebkitDocument).webkitExitFullscreen?.().catch(() => {
|
||||||
/* nothing to do */
|
/* nothing to do */
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description utility hook to around setInterval
|
* @description utility hook to around setInterval
|
||||||
|
|||||||
@@ -1,53 +1,46 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
/**
|
const STORAGE_EVENT = 'ontime-storage';
|
||||||
* @description utility hook to handle state in local storage
|
|
||||||
* @param key
|
|
||||||
* @param initialValue
|
|
||||||
*/
|
|
||||||
export const useLocalStorage = <T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] => {
|
|
||||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
||||||
try {
|
|
||||||
const item = window.localStorage.getItem(`ontime-${key}`);
|
|
||||||
return item ? JSON.parse(item) : initialValue;
|
|
||||||
} catch (error) {
|
|
||||||
return initialValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
function getSnapshot(key: string): string | null {
|
||||||
const handleStorageChange = (event: StorageEvent) => {
|
try {
|
||||||
if (event.storageArea === window.localStorage && event.key === key) {
|
return window.localStorage.getItem(`ontime-${key}`);
|
||||||
try {
|
} catch {
|
||||||
const newValue = event.newValue ? JSON.parse(event.newValue) : initialValue;
|
return null;
|
||||||
setStoredValue(newValue);
|
}
|
||||||
} catch (_) {
|
}
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('storage', handleStorageChange);
|
function getParsedJson<T>(localStorageValue: string | null, initialValue: T): T {
|
||||||
|
try {
|
||||||
|
return localStorageValue ? JSON.parse(localStorageValue) : initialValue;
|
||||||
|
} catch {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
export const useLocalStorage = <T>(key: string, initialValue: T) => {
|
||||||
window.removeEventListener('storage', handleStorageChange);
|
const localStorageValue = useSyncExternalStore(subscribe, () => getSnapshot(key));
|
||||||
};
|
const parsedLocalStorageValue = getParsedJson(localStorageValue, initialValue);
|
||||||
}, [initialValue, key]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Set value to local storage
|
* @description Set value to local storage
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
const setValue = (value: T | ((val: T) => T)) => {
|
const setLocalStorageValue = (value: T | ((val: T) => T)) => {
|
||||||
try {
|
// Allow value to be a function so we have same API as useState
|
||||||
// Allow value to be a function so we have same API as useState
|
const valueToStore = value instanceof Function ? value(parsedLocalStorageValue) : value;
|
||||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
||||||
|
|
||||||
setStoredValue(valueToStore);
|
localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
||||||
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
window.dispatchEvent(new StorageEvent(STORAGE_EVENT));
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
return [storedValue, setValue];
|
|
||||||
|
return [parsedLocalStorageValue, setLocalStorageValue] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function subscribe(callback: () => void) {
|
||||||
|
window.addEventListener(STORAGE_EVENT, callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(STORAGE_EVENT, callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { MouseEvent, SyntheticEvent, TouchEvent, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
type LongPressOptions = {
|
||||||
|
threshold?: number;
|
||||||
|
onStart?: (e: SyntheticEvent) => void;
|
||||||
|
onFinish?: (e: SyntheticEvent) => void;
|
||||||
|
onCancel?: (e: SyntheticEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LongPressFns = {
|
||||||
|
onMouseDown: (e: MouseEvent) => void;
|
||||||
|
onMouseUp: (e: MouseEvent) => void;
|
||||||
|
onMouseLeave: (e: MouseEvent) => void;
|
||||||
|
onTouchStart: (e: TouchEvent) => void;
|
||||||
|
onTouchEnd: (e: TouchEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useLongPress(callback: () => void, options: LongPressOptions = {}): LongPressFns {
|
||||||
|
const { threshold = 400, onStart, onFinish, onCancel } = options;
|
||||||
|
const isLongPressActive = useRef(false);
|
||||||
|
const isPressed = useRef(false);
|
||||||
|
const timerId = useRef<NodeJS.Timer>();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const start = (event: SyntheticEvent) => {
|
||||||
|
if (onStart) {
|
||||||
|
onStart(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
isPressed.current = true;
|
||||||
|
timerId.current = setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
isLongPressActive.current = true;
|
||||||
|
}, threshold);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = (event: SyntheticEvent) => {
|
||||||
|
if (isLongPressActive.current) {
|
||||||
|
if (onFinish) {
|
||||||
|
onFinish(event);
|
||||||
|
}
|
||||||
|
} else if (isPressed.current) {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLongPressActive.current = false;
|
||||||
|
isPressed.current = false;
|
||||||
|
|
||||||
|
if (timerId.current) {
|
||||||
|
clearTimeout(timerId.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onMouseDown: start,
|
||||||
|
onMouseUp: cancel,
|
||||||
|
onMouseLeave: cancel,
|
||||||
|
onTouchStart: start,
|
||||||
|
onTouchEnd: cancel,
|
||||||
|
};
|
||||||
|
}, [callback, threshold, onCancel, onFinish, onStart]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Shamelessly from https://ahooks.js.org/hooks/use-memoized-fn/
|
||||||
|
* Interesting technique discussed by Dan Abramov
|
||||||
|
* https://overreacted.io/making-setinterval-declarative-with-react-hooks/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import { isDev } from '../api/apiConstants';
|
||||||
|
|
||||||
|
type noop = (this: any, ...args: any[]) => any;
|
||||||
|
|
||||||
|
type PickFunction<T extends noop> = (this: ThisParameterType<T>, ...args: Parameters<T>) => ReturnType<T>;
|
||||||
|
|
||||||
|
export const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function';
|
||||||
|
|
||||||
|
export default function useMemoisedFn<T extends noop>(fn: T) {
|
||||||
|
if (isDev) {
|
||||||
|
if (!isFunction(fn)) {
|
||||||
|
console.error(`useMemoisedFn expected function as parameter, got ${typeof fn}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fnRef = useRef<T>(fn);
|
||||||
|
|
||||||
|
// why not write `fnRef.current = fn`?
|
||||||
|
// https://github.com/alibaba/hooks/issues/728
|
||||||
|
fnRef.current = useMemo(() => fn, [fn]);
|
||||||
|
|
||||||
|
const memoizedFn = useRef<PickFunction<T>>();
|
||||||
|
if (!memoizedFn.current) {
|
||||||
|
memoizedFn.current = function (this, ...args) {
|
||||||
|
return fnRef.current.apply(this, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return memoizedFn.current as T;
|
||||||
|
}
|
||||||
@@ -1,61 +1,73 @@
|
|||||||
import { RuntimeStore } from 'ontime-types';
|
import { RuntimeStore, SimpleDirection, SimplePlayback } from 'ontime-types';
|
||||||
|
|
||||||
import { deepCompare, useRuntimeStore } from '../stores/runtime';
|
import { useRuntimeStore } from '../stores/runtime';
|
||||||
import { socketSendJson } from '../utils/socket';
|
import { socketSendJson } from '../utils/socket';
|
||||||
|
|
||||||
export const useRundownEditor = () => {
|
export const useRundownEditor = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
playback: state.playback,
|
playback: state.timer.playback,
|
||||||
selectedEventId: state.loaded.selectedEventId,
|
selectedEventId: state.eventNow?.id ?? null,
|
||||||
nextEventId: state.loaded.nextEventId,
|
nextEventId: state.eventNext?.id ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector, deepCompare);
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOperator = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
playback: state.timer.playback,
|
||||||
|
selectedEventId: state.eventNow?.id ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useMessageControl = () => {
|
export const useMessageControl = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
timerMessage: state.timerMessage,
|
timer: state.message.timer,
|
||||||
publicMessage: state.publicMessage,
|
public: state.message.public,
|
||||||
lowerMessage: state.lowerMessage,
|
lower: state.message.lower,
|
||||||
|
external: state.message.external,
|
||||||
onAir: state.onAir,
|
onAir: state.onAir,
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector, deepCompare);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setMessage = {
|
export const setMessage = {
|
||||||
presenterText: (payload: string) => socketSendJson('set-timer-message-text', payload),
|
timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }),
|
||||||
presenterVisible: (payload: boolean) => socketSendJson('set-timer-message-visible', payload),
|
timerVisible: (payload: boolean) => socketSendJson('message', { timer: { visible: payload } }),
|
||||||
publicText: (payload: string) => socketSendJson('set-public-message-text', payload),
|
publicText: (payload: string) => socketSendJson('message', { public: { text: payload } }),
|
||||||
publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload),
|
publicVisible: (payload: boolean) => socketSendJson('message', { public: { visible: payload } }),
|
||||||
lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload),
|
lowerText: (payload: string) => socketSendJson('message', { lower: { text: payload } }),
|
||||||
lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload),
|
lowerVisible: (payload: boolean) => socketSendJson('message', { lower: { visible: payload } }),
|
||||||
onAir: (payload: boolean) => socketSendJson('set-onAir', payload),
|
externalText: (payload: string) => socketSendJson('message', { external: { visible: payload } }),
|
||||||
timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', payload),
|
externalVisible: (payload: boolean) => socketSendJson('message', { external: { visible: payload } }),
|
||||||
timerBlackout: (payload: boolean) => socketSendJson('set-timer-blackout', payload),
|
onAir: (payload: boolean) => socketSendJson('onAir', payload),
|
||||||
|
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
|
||||||
|
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePlaybackControl = () => {
|
export const usePlaybackControl = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
playback: state.playback,
|
playback: state.timer.playback,
|
||||||
selectedEventIndex: state.loaded.selectedEventIndex,
|
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||||
numEvents: state.loaded.numEvents,
|
numEvents: state.runtime.numEvents,
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector, deepCompare);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setPlayback = {
|
export const setPlayback = {
|
||||||
start: () => socketSendJson('start'),
|
start: () => socketSendJson('start'),
|
||||||
pause: () => socketSendJson('pause'),
|
pause: () => socketSendJson('pause'),
|
||||||
roll: () => socketSendJson('roll'),
|
roll: () => socketSendJson('roll'),
|
||||||
startNext: () => socketSendJson('start-next'),
|
startNext: () => socketSendJson('start', { next: true }),
|
||||||
previous: () => {
|
previous: () => {
|
||||||
socketSendJson('previous');
|
socketSendJson('load', { previous: true });
|
||||||
},
|
},
|
||||||
next: () => {
|
next: () => {
|
||||||
socketSendJson('next');
|
socketSendJson('load', { next: true });
|
||||||
},
|
},
|
||||||
stop: () => {
|
stop: () => {
|
||||||
socketSendJson('stop');
|
socketSendJson('stop');
|
||||||
@@ -63,37 +75,61 @@ export const setPlayback = {
|
|||||||
reload: () => {
|
reload: () => {
|
||||||
socketSendJson('reload');
|
socketSendJson('reload');
|
||||||
},
|
},
|
||||||
delay: (amount: number) => {
|
addTime: (amount: number) => {
|
||||||
socketSendJson('delay', amount);
|
socketSendJson('addtime', amount);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInfoPanel = () => {
|
export const useInfoPanel = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
titles: state.titles,
|
eventNow: state.eventNow,
|
||||||
playback: state.playback,
|
eventNext: state.eventNext,
|
||||||
selectedEventIndex: state.loaded.selectedEventIndex,
|
playback: state.timer.playback,
|
||||||
numEvents: state.loaded.numEvents,
|
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||||
|
numEvents: state.runtime.numEvents,
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector, deepCompare);
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExtraTimerTime = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => state.timer1.current;
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExtraTimerControl = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
playback: state.timer1.playback,
|
||||||
|
direction: state.timer1.direction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setExtraTimer = {
|
||||||
|
start: () => socketSendJson('extratimer', SimplePlayback.Start),
|
||||||
|
pause: () => socketSendJson('extratimer', SimplePlayback.Pause),
|
||||||
|
stop: () => socketSendJson('extratimer', SimplePlayback.Stop),
|
||||||
|
setDirection: (direction: SimpleDirection) => socketSendJson('extratimer', { direction }),
|
||||||
|
setTime: (time: number) => socketSendJson('extratimer', { settime: time }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCuesheet = () => {
|
export const useCuesheet = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
playback: state.playback,
|
playback: state.timer.playback,
|
||||||
selectedEventId: state.loaded.selectedEventId,
|
selectedEventId: state.eventNow?.id ?? null,
|
||||||
selectedEventIndex: state.loaded.selectedEventIndex,
|
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||||
numEvents: state.loaded.numEvents,
|
numEvents: state.runtime.numEvents,
|
||||||
titleNow: state.titles.titleNow,
|
titleNow: state.eventNow?.title || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector, deepCompare);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setEventPlayback = {
|
export const setEventPlayback = {
|
||||||
loadEvent: (eventId: string) => socketSendJson('loadid', eventId),
|
loadEvent: (id: string) => socketSendJson('load', { id }),
|
||||||
startEvent: (eventId: string) => socketSendJson('startid', eventId),
|
startEvent: (id: string) => socketSendJson('start', { id }),
|
||||||
start: () => socketSendJson('start'),
|
start: () => socketSendJson('start'),
|
||||||
pause: () => socketSendJson('pause'),
|
pause: () => socketSendJson('pause'),
|
||||||
};
|
};
|
||||||
@@ -103,7 +139,37 @@ export const useTimer = () => {
|
|||||||
...state.timer,
|
...state.timer,
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector, deepCompare);
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useClock = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
clock: state.clock,
|
||||||
|
});
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Used by the progress bar components */
|
||||||
|
export const useProgressData = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
addedTime: state.timer.addedTime,
|
||||||
|
current: state.timer.current,
|
||||||
|
duration: state.timer.duration,
|
||||||
|
timeWarning: state.eventNow?.timeWarning ?? null,
|
||||||
|
timeDanger: state.eventNow?.timeDanger ?? null,
|
||||||
|
});
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setClientName = (newName: string) => socketSendJson('set-client-name', newName);
|
export const setClientName = (newName: string) => socketSendJson('set-client-name', newName);
|
||||||
|
|
||||||
|
export const useRuntimeOverview = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
playback: state.timer.playback,
|
||||||
|
clock: state.clock,
|
||||||
|
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||||
|
numEvents: state.runtime.numEvents,
|
||||||
|
});
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,26 +1,6 @@
|
|||||||
export const httpPlaceholder = {
|
import { HttpSettings } from 'ontime-types';
|
||||||
onLoad: {
|
|
||||||
url: '',
|
export const httpPlaceholder: HttpSettings = {
|
||||||
enabled: false,
|
enabledOut: false,
|
||||||
},
|
subscriptions: [],
|
||||||
onStart: {
|
|
||||||
url: '',
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
onUpdate: {
|
|
||||||
url: '',
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
onPause: {
|
|
||||||
url: '',
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
onStop: {
|
|
||||||
url: '',
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
onFinish: {
|
|
||||||
url: '',
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import { Settings } from 'ontime-types';
|
import { GetInfo } from 'ontime-types';
|
||||||
|
|
||||||
type NetworkInterfaceType = {
|
import { oscPlaceholderSettings } from './OscSettings';
|
||||||
name: string;
|
|
||||||
address: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InfoType = {
|
export const ontimePlaceholderInfo: GetInfo = {
|
||||||
networkInterfaces: NetworkInterfaceType[];
|
|
||||||
settings: Pick<Settings, 'version' | 'serverPort'>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ontimePlaceholderInfo: InfoType = {
|
|
||||||
networkInterfaces: [],
|
networkInterfaces: [],
|
||||||
settings: {
|
version: '2.0.0',
|
||||||
version: 2,
|
serverPort: 4001,
|
||||||
serverPort: 4001,
|
osc: oscPlaceholderSettings,
|
||||||
},
|
cssOverride: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Settings } from 'ontime-types';
|
|||||||
|
|
||||||
export const ontimePlaceholderSettings: Settings = {
|
export const ontimePlaceholderSettings: Settings = {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
version: 2,
|
version: '2.0.0',
|
||||||
serverPort: 4001,
|
serverPort: 4001,
|
||||||
editorKey: null,
|
editorKey: null,
|
||||||
operatorKey: null,
|
operatorKey: null,
|
||||||
|
|||||||
@@ -1,23 +1,10 @@
|
|||||||
import { OSCSettings } from 'ontime-types';
|
import { OSCSettings } from 'ontime-types';
|
||||||
|
|
||||||
// in the placeholder, we pass strings to satisfy input type
|
export const oscPlaceholderSettings: OSCSettings = {
|
||||||
export interface PlaceholderSettings extends Omit<OSCSettings, 'portIn' | 'portOut'> {
|
portIn: 8888,
|
||||||
portIn: string;
|
portOut: 9999,
|
||||||
portOut: string;
|
targetIP: '127.0.0.1',
|
||||||
}
|
|
||||||
|
|
||||||
export const oscPlaceholderSettings: PlaceholderSettings = {
|
|
||||||
portIn: '',
|
|
||||||
portOut: '',
|
|
||||||
targetIP: '',
|
|
||||||
enabledIn: false,
|
enabledIn: false,
|
||||||
enabledOut: false,
|
enabledOut: false,
|
||||||
subscriptions: {
|
subscriptions: [],
|
||||||
onLoad: [],
|
|
||||||
onStart: [],
|
|
||||||
onPause: [],
|
|
||||||
onStop: [],
|
|
||||||
onUpdate: [],
|
|
||||||
onFinish: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { EventData } from 'ontime-types';
|
import { ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
export const eventDataPlaceholder: EventData = {
|
export const projectDataPlaceholder: ProjectData = {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
publicUrl: '',
|
publicUrl: '',
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import { Playback, TimerType } from 'ontime-types';
|
import { MaybeNumber, Playback, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
export type TimeManagerType = {
|
// first set extends TimerState
|
||||||
clock: number;
|
export type ViewExtendedTimer = {
|
||||||
current: null | number;
|
|
||||||
elapsed: null | number;
|
|
||||||
duration: null | number;
|
|
||||||
timerBehaviour?: string;
|
|
||||||
timerType: TimerType;
|
|
||||||
expectedFinish: null | number;
|
|
||||||
addedTime: number;
|
addedTime: number;
|
||||||
startedAt: null | number;
|
current: MaybeNumber;
|
||||||
finishedAt: null | number;
|
duration: MaybeNumber;
|
||||||
secondaryTimer: null | number;
|
elapsed: MaybeNumber;
|
||||||
|
expectedFinish: MaybeNumber;
|
||||||
finished: boolean;
|
finishedAt: MaybeNumber;
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
|
secondaryTimer: MaybeNumber;
|
||||||
|
startedAt: MaybeNumber;
|
||||||
|
|
||||||
|
clock: number;
|
||||||
|
timeDanger: MaybeNumber;
|
||||||
|
timeWarning: MaybeNumber;
|
||||||
|
timerType: TimerType;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ export type OverridableOptions = {
|
|||||||
hideEndMessage?: boolean;
|
hideEndMessage?: boolean;
|
||||||
language?: string;
|
language?: string;
|
||||||
showProgressBar?: boolean;
|
showProgressBar?: boolean;
|
||||||
|
hideTimerSeconds?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ export const viewsSettingsPlaceholder: ViewSettings = {
|
|||||||
overrideStyles: false,
|
overrideStyles: false,
|
||||||
normalColor: '#ffffffcc',
|
normalColor: '#ffffffcc',
|
||||||
warningColor: '#FFAB33',
|
warningColor: '#FFAB33',
|
||||||
warningThreshold: 120000,
|
|
||||||
dangerColor: '#ED3333',
|
dangerColor: '#ED3333',
|
||||||
dangerThreshold: 60000,
|
|
||||||
endMessage: '',
|
endMessage: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { QueryClient } from '@tanstack/react-query';
|
|||||||
export const ontimeQueryClient = new QueryClient({
|
export const ontimeQueryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
cacheTime: 1000 * 60 * 10, // 10 min
|
gcTime: 1000 * 60 * 10, // 10 min
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,63 +8,29 @@ export enum AppMode {
|
|||||||
const appModeKey = 'ontime-app-mode';
|
const appModeKey = 'ontime-app-mode';
|
||||||
|
|
||||||
function getModeFromSession() {
|
function getModeFromSession() {
|
||||||
return localStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
|
return sessionStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persistModeToSession(mode: AppMode) {
|
function persistModeToSession(mode: AppMode) {
|
||||||
localStorage.setItem(appModeKey, mode);
|
sessionStorage.setItem(appModeKey, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppModeStore = {
|
type AppModeStore = {
|
||||||
mode: AppMode;
|
mode: AppMode;
|
||||||
cursor: string | null;
|
|
||||||
editId: string | null;
|
|
||||||
setMode: (mode: AppMode) => void;
|
setMode: (mode: AppMode) => void;
|
||||||
setCursor: (id: string | null, isEditable?: boolean) => void;
|
cursor: string | null;
|
||||||
setEditId: (id: string | null) => void;
|
setCursor: (cursor: string | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAppMode = create<AppModeStore>()((set) => ({
|
export const useAppMode = create<AppModeStore>()((set) => ({
|
||||||
mode: getModeFromSession(),
|
mode: getModeFromSession(),
|
||||||
cursor: null,
|
cursor: null,
|
||||||
editId: null,
|
setMode: (mode: AppMode) => {
|
||||||
setMode: (mode: AppMode) =>
|
persistModeToSession(mode);
|
||||||
set((state) => {
|
|
||||||
persistModeToSession(mode);
|
return set(() => {
|
||||||
return mode === AppMode.Edit
|
return { mode };
|
||||||
? {
|
});
|
||||||
editId: state.cursor,
|
},
|
||||||
mode: mode,
|
setCursor: (cursor: string | null) => set(() => ({ cursor })),
|
||||||
}
|
|
||||||
: {
|
|
||||||
editId: null,
|
|
||||||
mode: mode,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
setCursor: (id: string | null, isEditable?: boolean) =>
|
|
||||||
set((state) => {
|
|
||||||
if (isEditable) {
|
|
||||||
return state.mode === AppMode.Edit
|
|
||||||
? {
|
|
||||||
cursor: id,
|
|
||||||
editId: id,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
cursor: id,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return { cursor: id, editId: null };
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
setEditId: (id: string | null) =>
|
|
||||||
set((state) => {
|
|
||||||
return state.mode === AppMode.Edit
|
|
||||||
? {
|
|
||||||
cursor: id,
|
|
||||||
editId: id,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
editId: id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ type EditorSettings = {
|
|||||||
showQuickEntry: boolean;
|
showQuickEntry: boolean;
|
||||||
startTimeIsLastEnd: boolean;
|
startTimeIsLastEnd: boolean;
|
||||||
defaultPublic: boolean;
|
defaultPublic: boolean;
|
||||||
showNif: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditorSettingsStore = {
|
type EditorSettingsStore = {
|
||||||
@@ -15,14 +14,12 @@ type EditorSettingsStore = {
|
|||||||
setShowQuickEntry: (showQuickEntry: boolean) => void;
|
setShowQuickEntry: (showQuickEntry: boolean) => void;
|
||||||
setStartTimeIsLastEnd: (startTimeIsLastEnd: boolean) => void;
|
setStartTimeIsLastEnd: (startTimeIsLastEnd: boolean) => void;
|
||||||
setDefaultPublic: (defaultPublic: boolean) => void;
|
setDefaultPublic: (defaultPublic: boolean) => void;
|
||||||
setShowNif: (showNif: boolean) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum EditorSettingsKeys {
|
enum EditorSettingsKeys {
|
||||||
ShowQuickEntry = 'ontime-show-quick-entry',
|
ShowQuickEntry = 'ontime-show-quick-entry',
|
||||||
StartTimeIsLastEnd = 'ontime-start-is-last-end',
|
StartTimeIsLastEnd = 'ontime-start-is-last-end',
|
||||||
DefaultPublic = 'ontime-default-public',
|
DefaultPublic = 'ontime-default-public',
|
||||||
ShowNif = 'ontime-show-nif',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEditorSettings = create<EditorSettingsStore>((set) => ({
|
export const useEditorSettings = create<EditorSettingsStore>((set) => ({
|
||||||
@@ -30,7 +27,6 @@ export const useEditorSettings = create<EditorSettingsStore>((set) => ({
|
|||||||
showQuickEntry: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, false),
|
showQuickEntry: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, false),
|
||||||
startTimeIsLastEnd: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, true),
|
startTimeIsLastEnd: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, true),
|
||||||
defaultPublic: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, true),
|
defaultPublic: booleanFromLocalStorage(EditorSettingsKeys.ShowQuickEntry, true),
|
||||||
showNif: booleanFromLocalStorage(EditorSettingsKeys.ShowNif, true),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setLocalEventSettings: (value) =>
|
setLocalEventSettings: (value) =>
|
||||||
@@ -58,10 +54,4 @@ export const useEditorSettings = create<EditorSettingsStore>((set) => ({
|
|||||||
localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(defaultPublic));
|
localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(defaultPublic));
|
||||||
return { eventSettings: { ...state.eventSettings, defaultPublic } };
|
return { eventSettings: { ...state.eventSettings, defaultPublic } };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setShowNif: (showNif) =>
|
|
||||||
set((state) => {
|
|
||||||
localStorage.setItem(EditorSettingsKeys.ShowNif, String(showNif));
|
|
||||||
return { eventSettings: { ...state.eventSettings, showNif } };
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,76 +1,65 @@
|
|||||||
import isEqual from 'react-fast-compare';
|
import isEqual from 'react-fast-compare';
|
||||||
import { Playback, RuntimeStore } from 'ontime-types';
|
import { Playback, RuntimeStore, SimpleDirection, SimplePlayback } from 'ontime-types';
|
||||||
import { useStore } from 'zustand';
|
import { createWithEqualityFn, useStoreWithEqualityFn } from 'zustand/traditional';
|
||||||
import { createStore } from 'zustand/vanilla';
|
|
||||||
|
|
||||||
export const runtimeStorePlaceholder = {
|
export const runtimeStorePlaceholder: RuntimeStore = {
|
||||||
|
clock: 0,
|
||||||
timer: {
|
timer: {
|
||||||
clock: 0,
|
addedTime: 0,
|
||||||
current: null,
|
current: null,
|
||||||
|
duration: null,
|
||||||
elapsed: null,
|
elapsed: null,
|
||||||
expectedFinish: null,
|
expectedFinish: null,
|
||||||
addedTime: 0,
|
|
||||||
startedAt: null,
|
|
||||||
finishedAt: null,
|
finishedAt: null,
|
||||||
|
playback: Playback.Stop,
|
||||||
secondaryTimer: null,
|
secondaryTimer: null,
|
||||||
selectedEventId: null,
|
startedAt: null,
|
||||||
duration: null,
|
|
||||||
timerType: null,
|
|
||||||
endAction: null,
|
|
||||||
},
|
|
||||||
playback: Playback.Stop,
|
|
||||||
timerMessage: {
|
|
||||||
text: '',
|
|
||||||
visible: false,
|
|
||||||
timerBlink: false,
|
|
||||||
timerBlackout: false,
|
|
||||||
},
|
|
||||||
publicMessage: {
|
|
||||||
text: '',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
lowerMessage: {
|
|
||||||
text: '',
|
|
||||||
visible: false,
|
|
||||||
},
|
},
|
||||||
onAir: false,
|
onAir: false,
|
||||||
loaded: {
|
message: {
|
||||||
|
timer: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
blink: false,
|
||||||
|
blackout: false,
|
||||||
|
},
|
||||||
|
public: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
lower: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
external: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
numEvents: 0,
|
numEvents: 0,
|
||||||
selectedEventIndex: null,
|
selectedEventIndex: null,
|
||||||
selectedEventId: null,
|
|
||||||
selectedPublicEventId: null,
|
|
||||||
nextEventId: null,
|
|
||||||
nextPublicEventId: null,
|
|
||||||
},
|
},
|
||||||
titles: {
|
eventNow: null,
|
||||||
titleNow: null,
|
eventNext: null,
|
||||||
subtitleNow: null,
|
publicEventNow: null,
|
||||||
presenterNow: null,
|
publicEventNext: null,
|
||||||
noteNow: null,
|
timer1: {
|
||||||
titleNext: null,
|
current: 0,
|
||||||
subtitleNext: null,
|
direction: SimpleDirection.CountUp,
|
||||||
presenterNext: null,
|
duration: 0,
|
||||||
noteNext: null,
|
playback: SimplePlayback.Stop,
|
||||||
},
|
|
||||||
titlesPublic: {
|
|
||||||
titleNow: null,
|
|
||||||
subtitleNow: null,
|
|
||||||
presenterNow: null,
|
|
||||||
noteNow: null,
|
|
||||||
titleNext: null,
|
|
||||||
subtitleNext: null,
|
|
||||||
presenterNext: null,
|
|
||||||
noteNext: null,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const runtime = createStore<RuntimeStore>(() => ({
|
const deepCompare = <T>(a: T, b: T) => isEqual(a, b);
|
||||||
...runtimeStorePlaceholder,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const deepCompare = <T>(a: T, b: T) => isEqual(a, b);
|
export const runtimeStore = createWithEqualityFn<RuntimeStore>(
|
||||||
|
() => ({
|
||||||
|
...runtimeStorePlaceholder,
|
||||||
|
}),
|
||||||
|
deepCompare,
|
||||||
|
);
|
||||||
|
|
||||||
export const useRuntimeStore = <T>(
|
export const useRuntimeStore = <T>(selector: (state: RuntimeStore) => T) =>
|
||||||
selector: (state: RuntimeStore) => T,
|
useStoreWithEqualityFn(runtimeStore, selector, deepCompare);
|
||||||
equalityFn?: (a: unknown, b: unknown) => boolean,
|
|
||||||
) => useStore(runtime, selector, equalityFn);
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user