mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 00:29:41 +00:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d1260321f | |||
| 7577f307bf | |||
| 997be35815 | |||
| f9b1880e88 | |||
| 8a1474e8d6 | |||
| d432f1e3ff | |||
| 9f207dd39a | |||
| 8b8b3347fb | |||
| 01c2ef4c4d | |||
| 10852eecdd | |||
| e221543dc7 | |||
| 01000f8837 | |||
| 9a03e4850a | |||
| 6bb3fb2931 | |||
| a86496ed27 | |||
| df8b76305e | |||
| ce14aabcd7 | |||
| f5e9a2549e | |||
| f4692db021 | |||
| dddb11ff40 | |||
| 6178ed1e4e | |||
| 8c5aaa0901 | |||
| b06ab15190 | |||
| a48be0f017 | |||
| c2280da61f | |||
| fe30752130 | |||
| a20d63451b | |||
| 93622e9aeb | |||
| 1568af97fc | |||
| 4bb836b6f2 | |||
| 8156da03d5 | |||
| dfade25a7c | |||
| 4b21745fc7 | |||
| a91a8a6358 | |||
| 4e6b833e10 | |||
| 3fdb1bd1c8 | |||
| 4148a3835b | |||
| 4fb9c42aef | |||
| dd0dcfc2a2 | |||
| d41d1b054d | |||
| 8b6e06150a | |||
| a9e0e2b091 | |||
| 0a8155b6e3 | |||
| 118c29e5c2 | |||
| 105f395e6e | |||
| 12d9e5922c | |||
| 63983fc39a | |||
| 41af3b7466 | |||
| 78b386742e | |||
| ad71793cf3 | |||
| 10d7273ec0 | |||
| 6dc2bebeee | |||
| d59885fc20 | |||
| d22cc48565 | |||
| 8196ea584d | |||
| 0c31438e44 | |||
| 10d3986832 | |||
| 088927bbbb | |||
| d87ba12b2e | |||
| 5b01329c37 | |||
| 8df419d835 | |||
| d7392b93d2 | |||
| 429df21557 | |||
| da7ef59216 | |||
| 474f1e2177 | |||
| fc5338903b | |||
| 11d06de133 | |||
| cbc2ae5116 | |||
| a6ff874a2a | |||
| 7ff75835ff | |||
| c890251dad | |||
| c1e2700c38 | |||
| 5a397c6da7 | |||
| c1377544a0 | |||
| 1a420a1ddd | |||
| 436a34aaee | |||
| 4848fdbee3 | |||
| 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 | |||
| 5b7045e6a5 | |||
| 7cc4364a63 | |||
| b4e73ff6b5 | |||
| 9031051465 | |||
| a15a9671d2 | |||
| ecd13fdc5a | |||
| 3fb862628c | |||
| 1f3133d4f3 | |||
| 64573aff8a | |||
| 655e9e8fb5 | |||
| bac2dc6b87 | |||
| 65a5aaa665 | |||
| c889d00e6b | |||
| 3753d51d85 | |||
| cb23b83b9d | |||
| a567fdc446 | |||
| 3d9a9a2226 | |||
| 9a4d659b04 | |||
| a5d8b91f34 | |||
| 5409c0ac6e | |||
| 3998b8927d | |||
| 7b9f64a630 | |||
| c5c0401ac4 | |||
| 96280e5932 | |||
| b2c01a141a | |||
| dc4ba4012c | |||
| d1e201ebb5 | |||
| a463cb491b | |||
| 6ffc314513 | |||
| f2632412aa | |||
| 062dc0c914 | |||
| fe9f12a14b | |||
| 3144888eb1 | |||
| 820eacde91 | |||
| 6886a3236f | |||
| bb541c551f | |||
| 3196bd7802 | |||
| ce0657cc6d | |||
| 44c2b1b5e3 |
@@ -11,13 +11,4 @@
|
|||||||
|
|
||||||
# Ignore git and cache folders
|
# Ignore git and cache folders
|
||||||
.git
|
.git
|
||||||
.gitignore
|
|
||||||
.cache
|
.cache
|
||||||
|
|
||||||
# Ignore build folders
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Ignore default volumes created by running docker compose up
|
|
||||||
ontime-db
|
|
||||||
ontime-styles
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
github: cpvalente
|
|
||||||
buy_me_a_coffee: cpvalente
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 183 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 336 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 660 KiB |
@@ -1,4 +1,4 @@
|
|||||||
name: Docker Image CI Ontime
|
name: Docker Image CI Ontime V2
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -13,14 +13,14 @@ jobs:
|
|||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Setup env
|
- name: Setup env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup Node.js environment
|
- name: Setup Node.js environment
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: 18.18.2
|
version: 16.16.0
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
name: Ontime build v2
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: [ "*" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_macos:
|
||||||
|
runs-on: macOS-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.18.2
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build project packages
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Electron - Build app
|
||||||
|
run: pnpm dist-mac
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
./apps/electron/dist/ontime-macOS-x64.dmg
|
||||||
|
./apps/electron/dist/ontime-macOS-arm64.dmg
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
build_windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.18.2
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build project packages
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Electron - Build app
|
||||||
|
run: pnpm dist-win
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: './apps/electron/dist/ontime-win64.exe'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
build_ubuntu:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.18.2
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build project packages
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Electron - Build app
|
||||||
|
run: pnpm dist-linux
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: './apps/electron/dist/ontime-linux.AppImage'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
name: Ontime build
|
name: Ontime build v3
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
# Only trigger manually for now for testing
|
||||||
tags: [ "*" ]
|
# push:
|
||||||
|
# tags: [ "*" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -30,14 +31,7 @@ jobs:
|
|||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Electron - Build app
|
- name: Electron - Build app
|
||||||
env:
|
|
||||||
APPLE_ID: ${{ secrets.APPLEID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLEIDPASS }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.TEAMID }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
run: pnpm dist-mac
|
run: pnpm dist-mac
|
||||||
timeout-minutes: 60
|
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
name: Ontime test v2
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: '*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
env:
|
||||||
|
CI: ''
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.18.2
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Run code quality per package
|
||||||
|
- name: React - Run linter + TypeScript checks
|
||||||
|
if: always()
|
||||||
|
run: pnpm lint && tsc --noEmit
|
||||||
|
working-directory: ./apps/client
|
||||||
|
|
||||||
|
- name: Server - Run linter + TypeScript checks
|
||||||
|
if: always()
|
||||||
|
run: pnpm lint && tsc --noEmit
|
||||||
|
working-directory: ./apps/server
|
||||||
|
|
||||||
|
- name: Utils - Run linter + TypeScript checks
|
||||||
|
if: always()
|
||||||
|
run: pnpm lint && tsc --noEmit
|
||||||
|
working-directory: ./packages/utils
|
||||||
|
|
||||||
|
- name: Types - Run linter
|
||||||
|
if: always()
|
||||||
|
run: pnpm lint
|
||||||
|
working-directory: ./packages/types
|
||||||
|
|
||||||
|
# We choose to run tests separately
|
||||||
|
- name: React - Run unit tests
|
||||||
|
if: always()
|
||||||
|
run: pnpm test:pipeline
|
||||||
|
working-directory: ./apps/client
|
||||||
|
|
||||||
|
- name: Server - Run unit tests
|
||||||
|
if: always()
|
||||||
|
run: pnpm test:pipeline
|
||||||
|
working-directory: ./apps/server
|
||||||
|
|
||||||
|
- name: Utils - Run unit tests
|
||||||
|
if: always()
|
||||||
|
run: pnpm test:pipeline
|
||||||
|
working-directory: ./packages/utils
|
||||||
|
|
||||||
|
e2e-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.18.2
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build client
|
||||||
|
run: pnpm build:local
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: pnpm e2e
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: automated-screenshots
|
||||||
|
path: automated-screenshots/
|
||||||
|
retention-days: 14
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
name: Ontime test
|
name: Ontime test v3
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
# Only trigger manually for now for testing
|
||||||
branches: '*'
|
# pull_request:
|
||||||
|
# branches: '*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -88,6 +88,8 @@ 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`
|
||||||
|
- __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
-15
@@ -1,14 +1,3 @@
|
|||||||
FROM node:18.18-alpine AS base
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
|
||||||
RUN corepack enable
|
|
||||||
|
|
||||||
FROM base AS builder
|
|
||||||
COPY . /app
|
|
||||||
WORKDIR /app
|
|
||||||
RUN pnpm install --frozen-lockfile \
|
|
||||||
&& pnpm turbo build:docker
|
|
||||||
|
|
||||||
FROM node:18.18-alpine
|
FROM node:18.18-alpine
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
@@ -20,12 +9,12 @@ ENV ONTIME_DATA=/external/
|
|||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
# Prepare UI
|
# Prepare UI
|
||||||
COPY --from=builder /app/apps/client/build ./client/
|
COPY /apps/client/build ./client/
|
||||||
|
|
||||||
# Prepare Backend
|
# Prepare Backend
|
||||||
COPY --from=builder /app/apps/server/dist/ ./server/
|
COPY /apps/server/dist/ ./server/
|
||||||
COPY ./demo-db/ ./preloaded-db/
|
COPY /demo-db/ ./preloaded-db/
|
||||||
COPY --from=builder /app/apps/server/src/external/ ./external/
|
COPY /apps/server/src/external/ ./external/
|
||||||
|
|
||||||
# Export default ports
|
# Export default ports
|
||||||
EXPOSE 4001/tcp 8888/udp 9999/udp
|
EXPOSE 4001/tcp 8888/udp 9999/udp
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml)
|
||||||

|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||

|
|
||||||
[](https://github.com/sponsors/cpvalente)
|
|
||||||
[](https://www.buymeacoffee.com/cpvalente)
|
|
||||||
|
|
||||||
## Download the latest releases here
|
## Download the latest releases here
|
||||||
|
|
||||||
@@ -13,162 +10,196 @@
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/cpvalente" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="32"></a>
|
|
||||||
|
|
||||||
# Ontime
|
# Ontime
|
||||||
|
|
||||||
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
|
Ontime is an application for creating and managing event running order and timers.
|
||||||
|
|
||||||
With Ontime, you can plan, track your schedule, manage automation and cross-department show information all in one place.
|
The user inputs a list of events along with scheduling and event information.
|
||||||
|
This will then populate a series of screens which are available to be rendered by any device in the Network.
|
||||||
|
|
||||||
Ontime is made by entertainment and broadcast engineers and used by
|
This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video outputs.
|
||||||
- Conference organisers
|
|
||||||
- Touring shows and receiving venues
|
|
||||||
- Broadcasters and streamers
|
|
||||||
- Theatres and opera houses
|
|
||||||
- Houses of worship
|
|
||||||
|
|
||||||
### For live environments
|

|
||||||
|
|
||||||
Ontime is designed for use in live environments. \
|

|
||||||
This guides the application into being flexible and efficiently integrating into different workflows.
|
|
||||||
|
|
||||||
### For teams
|
|
||||||
|
|
||||||
All information added in Ontime is shared with the production team and other software / hardware in your workflow. \
|
|
||||||
Ontime also improves team collaboration with dedicated views for cuesheets and operators, and for public and production
|
|
||||||
signage.
|
|
||||||
|
|
||||||
### Simple infrastructure
|
|
||||||
|
|
||||||
All the data is distributed over the network, making its distribution and infrastructure flexible and cheap. \
|
|
||||||
With the availability of the docker image, you can also leverage IT infrastructure to make Ontime available online for
|
|
||||||
your team and clients.
|
|
||||||
|
|
||||||
Ontime is made by video engineers and entertainment technicians.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[Read the docs to learn more](https://docs.getontime.no)
|
[Read the docs to learn more](https://docs.getontime.no)
|
||||||
|
|
||||||
## Using Ontime
|
## Using Ontime
|
||||||
|
|
||||||
Ontime can be started by downloading the latest release for your platform. \
|
Once installed and running, Ontime starts a background server that is the heart of all processes.
|
||||||
Alternatively you can also use the docker image, available at [Docker Hub](https://hub.docker.com/r/getontime/ontime)
|
From the app, you can add / edit your running order and control the timer playback.
|
||||||
|
|
||||||
Once installed and running, any device in the network has access to Ontime.
|
Any device with a browser in the same network can choose one of the supported views to render the available data.
|
||||||
|
This is done by reaching the ontime server at the _default port 4001_ eg: `localhost:4001`
|
||||||
|
or `192.168.1.3:4001`
|
||||||
|
<br />
|
||||||
|
You can then use the menu in the top left corner to select the desired view.
|
||||||
|
The menu will be initially hidden until there is mouse interaction.
|
||||||
|
|
||||||
Ontime provides different screens which allow for different types of interactions with the data. These are called
|
In the case of unattended machines or automation, it is possible to use different URL to recall
|
||||||
views. \
|
individual views and extend view settings using the URL presets feature
|
||||||
Each view in Ontime focuses on empowering a specific role or achieving a particular task.
|
|
||||||
|
|
||||||
You can access the different views by reaching the ontime server, in your browser, at (_default port
|
|
||||||
4001_) `http://localhost:4001` or `http://192.168.1.3:4001`
|
|
||||||
|
|
||||||
```
|
```
|
||||||
For the backstage views
|
For the presentation views
|
||||||
-------------------------------------------------------------
|
-------------------------------------------------------------
|
||||||
|
IP.ADDRESS:4001 > Web server default to presenter timer view
|
||||||
IP.ADDRESS:4001/timer > Presenter / Stage timer view
|
IP.ADDRESS:4001/timer > Presenter / Stage timer view
|
||||||
IP.ADDRESS:4001/minimal > Simple timer view
|
IP.ADDRESS:4001/minimal > Simple timer view
|
||||||
IP.ADDRESS:4001/clock > Simple clock view
|
IP.ADDRESS:4001/clock > Simple clock view
|
||||||
IP.ADDRESS:4001/backstage > Stage Manager / Backstage view
|
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
|
||||||
IP.ADDRESS:4001/countdown > Countdown to anything
|
IP.ADDRESS:4001/public > Public / Foyer view
|
||||||
|
IP.ADDRESS:4001/lower > Lower Thirds
|
||||||
IP.ADDRESS:4001/studio > Studio Clock
|
IP.ADDRESS:4001/studio > Studio Clock
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
For the public views
|
For management views
|
||||||
-------------------------------------------------------------
|
|
||||||
IP.ADDRESS:4001/public > Public / Foyer view
|
|
||||||
IP.ADDRESS:4001/lower > Lower Thirds
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
For production views
|
|
||||||
-------------------------------------------------------------
|
-------------------------------------------------------------
|
||||||
IP.ADDRESS:4001/editor > the control interface, same as the app
|
IP.ADDRESS:4001/editor > the control interface, same as the app
|
||||||
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
|
IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration
|
||||||
IP.ADDRESS:4001/operator > automated views for operators
|
|
||||||
```
|
```
|
||||||
|
|
||||||
More documentation is available [in our docs](https://docs.getontime.no)
|
More documentation is available [in our docs](https://docs.getontime.no)
|
||||||
|
|
||||||
## Main features
|
## Feature List (in no specific order)
|
||||||
|
|
||||||
* [x] Distribute data over network and render it in the browser
|
- [x] Distribute data over network and render it in the browser
|
||||||
* [x] Collaborative
|
- [x] Different screen types
|
||||||
* [x] Extendable
|
- Stage Timer
|
||||||
* [x] Send messages to different screen types
|
- Minimal Timer
|
||||||
* [x] Differentiate between backstage and public data
|
- Clock
|
||||||
* [x] Workflow for managing delays
|
- Backstage Info
|
||||||
* [x] Rich protocol integrations for Control and Feedback
|
- Public Info
|
||||||
* [x] For servers: use docker to run Ontime in in a server, configure from a browser anywhere
|
- Studio Clock
|
||||||
* [x] Multi-platform (available on Windows, MacOS and Linux)
|
- Countdown
|
||||||
* [x] Companion integration [follow link](https://bitfocus.io/connections/getontime-ontime)
|
- [Make your own?](#make-your-own-viewer)
|
||||||
|
- [x] Configurable Lower Thirds
|
||||||
|
- [x] Collaborative editing with the cuesheet view
|
||||||
|
- [x] Send live messages to different screen types
|
||||||
|
- [x] Differentiate between backstage and public data
|
||||||
|
- [x] Workflow for managing delays
|
||||||
|
- [x] Rich protocol integrations for Control and Feedback
|
||||||
|
- OSC (Open Sound Control)
|
||||||
|
- HTTP
|
||||||
|
- WebSockets
|
||||||
|
- [x] Roll mode: run standalone using the system clock
|
||||||
|
- [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally
|
||||||
|
- [x] [Countdown to anything!](https://docs.getontime.no/features/count-to-anything/): have
|
||||||
|
a countdown to any scheduled event
|
||||||
|
- [x] Multi-platform (available on Windows, MacOS and Linux)
|
||||||
|
- [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime)
|
||||||
|
|
||||||
|
## Unopinionated
|
||||||
|
|
||||||
|
We want Ontime to be unique by targeting freelancers instead of roles.
|
||||||
|
|
||||||
|
We believe most freelancers work in different fields and we want to give you a tool that you can leverage across your
|
||||||
|
many environments and workflows.
|
||||||
|
|
||||||
|
We are not interested in forcing workflows and have made Ontime so, it is flexible to whichever way you would like to
|
||||||
|
work.
|
||||||
|
|
||||||
|
## Rich APIs for workflow integrations
|
||||||
|
|
||||||
|
The app is currently being developed for a broad user base, from broadcast to entertainment and
|
||||||
|
conference halls.
|
||||||
|
|
||||||
|
Taking advantage of the integrations, we currently use Ontime with:
|
||||||
|
|
||||||
|
- `disguise`: trigger Ontime from d3's timeline using the **OSC API**, and **render views** using d3's
|
||||||
|
webmodule
|
||||||
|
- `OBS`: **render views** using the Browser Module
|
||||||
|
- `QLab`: trigger Ontime using **OSC API**
|
||||||
|
- `Companion`: Ontime has a **companion module**. Issue report and feature requests should be done
|
||||||
|
in the [repository getontime/ontime](https://github.com/bitfocus/companion-module-getontime-ontime)
|
||||||
|
|
||||||
|
### Make your own viewer
|
||||||
|
|
||||||
|
Ontime broadcasts its data over WebSockets. This allows you to consume its data outside the application.
|
||||||
|
|
||||||
|
Writing a new view for the browser can be done with basic knowledge of HTML + CSS + Javascript (or any other language
|
||||||
|
that can run in the browser).
|
||||||
|
<br />
|
||||||
|
We have prepared a few resources to help here:
|
||||||
|
- Shipped with Ontime there is a small clock to get you started, it is available at `http://localhost:4001/external/demo` and the [code can be found here](https://github.com/cpvalente/ontime/tree/master/apps/server/src/external/demo)
|
||||||
|
- See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a template on
|
||||||
|
how to get you started
|
||||||
|
- See information about the [Websocket API](https://docs.getontime.no/api/osc-and-ws/)
|
||||||
|
<br />
|
||||||
|
More information [in the docs](https://docs.getontime.no/features/custom-views/)
|
||||||
|
|
||||||
|
|
||||||
|
### Headless run️
|
||||||
|
|
||||||
|
You can self-host and run Ontime in a docker image.
|
||||||
|
|
||||||
|
The docker image along with documentation is [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
|
||||||
|
|
||||||
|
If you want to run this image in a Raspberry Pi, please see [the docs](https://docs.getontime.no/additional-notes/use-with-rpi/)
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### Continued development
|
### Continued development
|
||||||
|
|
||||||
Ontime is under active development. We continue adding and tweaking features in collaboration with users.
|
Several features are planned in the roadmap, and we continuously adjust this to match how users interact with the app.
|
||||||
|
<br />
|
||||||
Have an idea? Reach out via [email](mail@getontime.no)
|
Have an idea? Reach out via [email](mail@getontime.no)
|
||||||
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
or [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
||||||
|
|
||||||
### Issues
|
### Issues
|
||||||
|
|
||||||
We use Github's issue tracking for bug reporting and feature requests. \
|
We use Github's issue tracking for bug reporting and feature requests. <br />
|
||||||
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
|
Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new).
|
||||||
|
|
||||||
#### Unsigned App
|
#### Unsigned App
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
in Windows
|
|
||||||
|
|
||||||
`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
|
||||||
|
|
||||||
|
`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`
|
||||||
|
|
||||||
We currently only sign MacOS releases. \
|
|
||||||
Purchasing the certificates for both Mac and Windows would mean a recurrent expense which we are not able to cover.
|
|
||||||
|
|
||||||
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` -> `Run Anyway`
|
- In Windows: click more and select "Run Anyway"
|
||||||
- In Linux: right-click the AppImage file: `Properties` -> `Permissions` -> `Allow Executing File as a Program`
|
- in macOS: the solution in macOS is different across versions, please refer to the [apple documentation](https://support.apple.com/en-gb/guide/mac-help/mh40616/mac)
|
||||||
|
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
|
||||||
|
File as a Program
|
||||||
|
|
||||||
If you have tips on how to improve this or would like to sponsor the code signing,
|
Long story short: Ontime app is unsigned. </br>Purchasing the certificates for both Mac and Windows
|
||||||
|
would mean a recurrent expense and is not a priority. This is unlikely to change in future. If you
|
||||||
|
have tips on how to improve this or would like to sponsor the code signing,
|
||||||
please [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
please [open an issue](https://github.com/cpvalente/ontime/issues/new)
|
||||||
|
|
||||||
## Contributing
|
#### Safari
|
||||||
|
|
||||||
|
There are known issues with Safari versions lower than 13:
|
||||||
|
|
||||||
|
- Spacing and text styles might have small inconsistencies
|
||||||
|
- Table view does not work
|
||||||
|
|
||||||
|
There is no plan for any further work on this.
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
|
Looking to contribute? All types of help are appreciated, from coding to testing and feature specification.
|
||||||
|
<br /><br />
|
||||||
If you are a developer and would like to contribute with 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 />
|
||||||
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)
|
||||||
|
|
||||||
## Links
|
# Help
|
||||||
|
|
||||||
See the [Ontime website](https://getontime.no) here and the link to the [documentation](https://docs.getontime.no)
|
Help is underway! ... and can be found [here](https://docs.getontime.no)
|
||||||
|
|
||||||
## License
|
# License
|
||||||
|
|
||||||
This project is licensed under the terms of the GNU GPL v3
|
This project is licensed under the terms of the GNU GPL v3
|
||||||
|
|
||||||
## Sponsor
|
|
||||||
|
|
||||||
You can help the development of this project or say thank you with a one time donation. \
|
|
||||||
See [the terms of donations](https://github.com/cpvalente/ontime/blob/master/.github/FUNDING.md).
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<br>
|
|
||||||
<a href="https://www.buymeacoffee.com/cpvalente" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="200"></a>
|
|
||||||
</p>
|
|
||||||
|
|||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
# Terms of sponsorship
|
|
||||||
Ontime does not exist as a fiscal entity. The sponsorship recipient is the project owner. In this light, contributors are funding the project owner's capacity to work on the project, not the project itself. As a contribution recipient, the code owner reserves the right to redistribute.
|
|
||||||
|
|
||||||
## Why sponsor
|
|
||||||
Ontime is and will remain free, regardless of your decision to contribute.
|
|
||||||
Regardless, you may choose to provide a monetary contribution:
|
|
||||||
|
|
||||||
### Improve user experience
|
|
||||||
Certain parts of the application user experience are tied to our capacity to pay for services. App signing and Google Sheets integration are good examples of this.
|
|
||||||
|
|
||||||
### Cool stuff
|
|
||||||
We may choose not to implement some features. This could be related to the complexity required, to the skill set, or to the fact that we just feel it doesn't improve the app for a significant percentage of users. In those cases, a feature may be considered "not worth it," regardless of its pertinence.
|
|
||||||
|
|
||||||
As a user, you can choose to sponsor a particular feature or donate to the app so that we can create bounties for features.
|
|
||||||
|
|
||||||
### Priorities
|
|
||||||
Monetary contributions are most immediately needed to offset the costs of presenting and distributing Ontime, i.e., hosting, domains, and certificates.
|
|
||||||
|
|
||||||
Additional funding will be considered according to the needs at the time.
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "3.0.3",
|
"version": "3.0.0-alpha",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@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",
|
||||||
"@fontsource/open-sans": "^5.0.28",
|
|
||||||
"@mantine/hooks": "^7.6.2",
|
"@mantine/hooks": "^7.6.2",
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@sentry/react": "^7.92.0",
|
"@sentry/react": "^7.92.0",
|
||||||
@@ -29,6 +28,7 @@
|
|||||||
"react-hook-form": "^7.49.2",
|
"react-hook-form": "^7.49.2",
|
||||||
"react-qr-code": "^2.0.12",
|
"react-qr-code": "^2.0.12",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
|
"typeface-open-sans": "^1.1.13",
|
||||||
"web-vitals": "^3.1.1",
|
"web-vitals": "^3.1.1",
|
||||||
"zustand": "^4.5.0"
|
"zustand": "^4.5.0"
|
||||||
},
|
},
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/vite-plugin": "^2.16.1",
|
"@sentry/vite-plugin": "^2.14.0",
|
||||||
"@tanstack/eslint-plugin-query": "^5.8.4",
|
"@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",
|
||||||
@@ -87,10 +87,10 @@
|
|||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.4.3",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.1.0",
|
||||||
"vite-plugin-compression2": "^0.12.0",
|
"vite-plugin-compression2": "^0.12.0",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"vite-tsconfig-paths": "^4.3.1",
|
"vite-tsconfig-paths": "^4.3.1",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
@@ -6,6 +7,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|||||||
import { ContextMenu } from './common/components/context-menu/ContextMenu';
|
import { ContextMenu } from './common/components/context-menu/ContextMenu';
|
||||||
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
||||||
import { AppContextProvider } from './common/context/AppContext';
|
import { AppContextProvider } from './common/context/AppContext';
|
||||||
|
import useElectronEvent from './common/hooks/useElectronEvent';
|
||||||
import { ontimeQueryClient } from './common/queryClient';
|
import { ontimeQueryClient } from './common/queryClient';
|
||||||
import { socketClientName } from './common/stores/connectionName';
|
import { socketClientName } from './common/stores/connectionName';
|
||||||
import { connectSocket } from './common/utils/socket';
|
import { connectSocket } from './common/utils/socket';
|
||||||
@@ -13,10 +15,38 @@ import theme from './theme/theme';
|
|||||||
import { TranslationProvider } from './translation/TranslationProvider';
|
import { TranslationProvider } from './translation/TranslationProvider';
|
||||||
import AppRouter from './AppRouter';
|
import AppRouter from './AppRouter';
|
||||||
|
|
||||||
|
// Load Open Sans typeface
|
||||||
|
// @ts-expect-error no types from font import
|
||||||
|
import('typeface-open-sans');
|
||||||
|
|
||||||
const preferredClientName = socketClientName.getState().name;
|
const preferredClientName = socketClientName.getState().name;
|
||||||
connectSocket(preferredClientName);
|
connectSocket(preferredClientName);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { isElectron, sendToElectron } = useElectronEvent();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyPress = (event: KeyboardEvent) => {
|
||||||
|
// handle held key
|
||||||
|
if (event.repeat) return;
|
||||||
|
// check if the alt key is pressed
|
||||||
|
if (event.altKey) {
|
||||||
|
if (event.code === 'KeyT') {
|
||||||
|
// ask to see debug
|
||||||
|
sendToElectron('set-window', 'show-dev');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isElectron) {
|
||||||
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (isElectron) {
|
||||||
|
document.removeEventListener('keydown', handleKeyPress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isElectron, sendToElectron]);
|
||||||
return (
|
return (
|
||||||
<ChakraProvider disableGlobalStyle resetCSS theme={theme}>
|
<ChakraProvider disableGlobalStyle resetCSS theme={theme}>
|
||||||
<QueryClientProvider client={ontimeQueryClient}>
|
<QueryClientProvider client={ontimeQueryClient}>
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ const dbPath = `${apiEntryUrl}/db`;
|
|||||||
/**
|
/**
|
||||||
* HTTP request to the current DB
|
* HTTP request to the current DB
|
||||||
*/
|
*/
|
||||||
async function getDb(fileName?: string): Promise<AxiosResponse<DatabaseModel>> {
|
async function getDb(): Promise<AxiosResponse<DatabaseModel>> {
|
||||||
return axios.post(`${dbPath}/download/`, { fileName });
|
return axios.get(`${dbPath}/download`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request download of the current project file
|
* Request download of the current project file
|
||||||
* @param fileName
|
* @param fileName
|
||||||
*/
|
*/
|
||||||
export async function downloadProject(fileName: string) {
|
export async function downloadProject(fileName: string = 'ontime-project') {
|
||||||
try {
|
try {
|
||||||
const { data, name } = await fileDownload(fileName);
|
const { data, name } = await fileDownload(fileName);
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ export async function getInfo(): Promise<GetInfo> {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async function fileDownload(fileName: string): Promise<{ data: DatabaseModel; name: string }> {
|
async function fileDownload(fileName: string): Promise<{ data: DatabaseModel; name: string }> {
|
||||||
const response = await getDb(fileName);
|
const response = await getDb();
|
||||||
|
|
||||||
const headerLine = response.headers['Content-Disposition'];
|
const headerLine = response.headers['Content-Disposition'];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { IoPowerOutline } from '@react-icons/all-files/io5/IoPowerOutline';
|
||||||
|
|
||||||
|
import { useEmitLog } from '../../stores/logger';
|
||||||
|
|
||||||
|
interface QuitIconBtnProps {
|
||||||
|
clickHandler: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quitBtnStyle = {
|
||||||
|
color: '#D20300', // $red-700
|
||||||
|
borderColor: '#D20300', // $red-700
|
||||||
|
_focus: { boxShadow: 'none' },
|
||||||
|
_hover: {
|
||||||
|
background: '#D20300', // $red-700
|
||||||
|
color: 'white',
|
||||||
|
_disabled: {
|
||||||
|
color: '#D20300', // $red-700
|
||||||
|
background: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_active: {
|
||||||
|
background: '#9A0000', // $red-1000
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
variant: 'outline',
|
||||||
|
isRound: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function QuitIconBtn(props: QuitIconBtnProps) {
|
||||||
|
const { clickHandler, disabled, ...rest } = props;
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { emitInfo } = useEmitLog();
|
||||||
|
const onClose = () => setIsOpen(false);
|
||||||
|
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.process?.type === 'renderer') {
|
||||||
|
window.ipcRenderer.on('user-request-shutdown', () => {
|
||||||
|
emitInfo('Shutdown request');
|
||||||
|
setIsOpen(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [emitInfo]);
|
||||||
|
|
||||||
|
const handleShutdown = useCallback(() => {
|
||||||
|
onClose();
|
||||||
|
clickHandler();
|
||||||
|
}, [clickHandler]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip label='Quit Application'>
|
||||||
|
<IconButton
|
||||||
|
aria-label='Quit Application'
|
||||||
|
size='lg'
|
||||||
|
icon={<IoPowerOutline />}
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
isDisabled={disabled}
|
||||||
|
{...quitBtnStyle}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
||||||
|
Ontime Shutdown
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogBody>This will shutdown the Ontime server. Are you sure?</AlertDialogBody>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button ref={cancelRef} onClick={onClose} variant='ghost'>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme='red' onClick={handleShutdown} ml={3}>
|
||||||
|
Shutdown
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { OptionWithoutGroup } from './ContextMenu';
|
|||||||
export const ContextMenuOption = ({ label, onClick, isDisabled, icon: Icon, withDivider }: OptionWithoutGroup) => (
|
export const ContextMenuOption = ({ label, onClick, isDisabled, icon: Icon, withDivider }: OptionWithoutGroup) => (
|
||||||
<>
|
<>
|
||||||
{withDivider && <MenuDivider />}
|
{withDivider && <MenuDivider />}
|
||||||
<MenuItem icon={<Icon style={{ fontSize: '1rem' }} />} onClick={onClick} isDisabled={isDisabled}>
|
<MenuItem icon={<Icon />} onClick={onClick} isDisabled={isDisabled}>
|
||||||
{label}
|
{label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import { RefObject, useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { Textarea, TextareaProps } from '@chakra-ui/react';
|
import { Textarea, TextareaProps } from '@chakra-ui/react';
|
||||||
// @ts-expect-error no types from library
|
// @ts-expect-error no types from library
|
||||||
import autosize from 'autosize/dist/autosize';
|
import autosize from 'autosize/dist/autosize';
|
||||||
|
|
||||||
export const AutoTextArea = (props: TextareaProps & { inputref: RefObject<unknown> }) => {
|
export const AutoTextArea = (props: TextareaProps) => {
|
||||||
const { value, inputref } = props;
|
const { value } = props;
|
||||||
|
|
||||||
|
const ref = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const node = inputref.current;
|
const node = ref.current;
|
||||||
autosize(inputref.current);
|
autosize(ref.current);
|
||||||
return () => {
|
return () => {
|
||||||
autosize.destroy(node);
|
autosize.destroy(node);
|
||||||
};
|
};
|
||||||
}, [inputref, value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
w='100%'
|
w='100%'
|
||||||
ref={inputref}
|
|
||||||
resize='none'
|
resize='none'
|
||||||
|
ref={ref}
|
||||||
transition='height none'
|
transition='height none'
|
||||||
variant='ontime-transparent'
|
variant='ontime-transparent'
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -25,5 +25,11 @@ export default function Swatch(props: SwatchProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (color === 'unkown') {
|
||||||
|
<div className={`${classes} ${style.center}`} onClick={handleClick}>
|
||||||
|
?
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
return <div className={classes} style={{ backgroundColor: `${color}` }} onClick={handleClick} />;
|
return <div className={classes} style={{ backgroundColor: `${color}` }} onClick={handleClick} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ interface ColourInputProps {
|
|||||||
value: string;
|
value: string;
|
||||||
name: 'colour';
|
name: 'colour';
|
||||||
handleChange: (newValue: 'colour', name: string) => void;
|
handleChange: (newValue: 'colour', name: string) => void;
|
||||||
|
isMultiple?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colours = [
|
const colours = [
|
||||||
'',
|
'',
|
||||||
|
'unkown',
|
||||||
'#FFCC78', // $orange-400
|
'#FFCC78', // $orange-400
|
||||||
'#FFAB33', // $orange-600
|
'#FFAB33', // $orange-600
|
||||||
'#77C785', // $green-400
|
'#77C785', // $green-400
|
||||||
@@ -27,7 +29,7 @@ const colours = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function SwatchSelect(props: ColourInputProps) {
|
export default function SwatchSelect(props: ColourInputProps) {
|
||||||
const { value, name, handleChange } = props;
|
const { value, name, handleChange, isMultiple } = props;
|
||||||
|
|
||||||
const setColour = useCallback(
|
const setColour = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
@@ -40,9 +42,15 @@ export default function SwatchSelect(props: ColourInputProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.list}>
|
<div className={style.list}>
|
||||||
{colours.map((colour) => (
|
{colours
|
||||||
<Swatch key={colour} color={colour} onClick={setColour} isSelected={value === colour} />
|
.filter((colour) => {
|
||||||
))}
|
// Only include unkown if it is multiple
|
||||||
|
if (!isMultiple && colour === 'unkown') return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((colour) => (
|
||||||
|
<Swatch key={colour} color={colour} onClick={setColour} isSelected={value === colour} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { Input, Textarea } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { EventEditorSubmitActions } from '../../../../features/rundown/event-editor/EventEditorWrapper';
|
||||||
|
import { Size } from '../../../models/Util.type';
|
||||||
|
|
||||||
|
import useReactiveTextInput from './useReactiveTextInput';
|
||||||
|
|
||||||
|
interface BaseProps {
|
||||||
|
isTextArea?: boolean;
|
||||||
|
isFullHeight?: boolean;
|
||||||
|
size?: Size;
|
||||||
|
field: EventEditorSubmitActions;
|
||||||
|
initialText?: string;
|
||||||
|
submitHandler: (field: EventEditorSubmitActions, newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextInputProps extends BaseProps {
|
||||||
|
isTextArea?: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResizeOptions = 'horizontal' | 'vertical' | 'none';
|
||||||
|
|
||||||
|
interface TextAreaProps extends BaseProps {
|
||||||
|
isTextArea: true;
|
||||||
|
resize?: ResizeOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputProps = TextInputProps | TextAreaProps;
|
||||||
|
|
||||||
|
export default function TextInput(props: InputProps) {
|
||||||
|
const { isTextArea, isFullHeight, size = 'sm', field, initialText = '', submitHandler } = props;
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||||
|
|
||||||
|
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
||||||
|
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
||||||
|
|
||||||
|
let resize: ResizeOptions = 'none';
|
||||||
|
if (isTextArea) {
|
||||||
|
resize = (props as TextAreaProps)?.resize ?? 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isTextArea ? (
|
||||||
|
<Textarea
|
||||||
|
ref={inputRef}
|
||||||
|
size={size}
|
||||||
|
resize={resize}
|
||||||
|
variant='ontime-filled'
|
||||||
|
{...textAreaProps}
|
||||||
|
style={{ height: isFullHeight ? '100%' : undefined }}
|
||||||
|
data-testid='input-textarea'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input ref={inputRef} size={size} variant='ontime-filled' {...textInputProps} data-testid='input-textfield' />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,20 +1,17 @@
|
|||||||
import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react';
|
||||||
import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks';
|
|
||||||
|
|
||||||
interface UseReactiveTextInputReturn {
|
interface UseReactiveTextInputReturn {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (event: ChangeEvent) => void;
|
onChange: (event: ChangeEvent) => void;
|
||||||
onBlur: (event: ChangeEvent) => void;
|
onBlur: (event: ChangeEvent) => void;
|
||||||
onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
onKeyDown: (event: KeyboardEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useReactiveTextInput(
|
export default function useReactiveTextInput(
|
||||||
initialText: string,
|
initialText: string,
|
||||||
submitCallback: (newValue: string) => void,
|
submitCallback: (newValue: string) => void,
|
||||||
ref: RefObject<HTMLInputElement>,
|
|
||||||
options?: {
|
options?: {
|
||||||
submitOnEnter?: boolean;
|
submitOnEnter?: boolean;
|
||||||
submitOnCtrlEnter?: boolean;
|
|
||||||
},
|
},
|
||||||
): UseReactiveTextInputReturn {
|
): UseReactiveTextInputReturn {
|
||||||
const [text, setText] = useState<string>(initialText);
|
const [text, setText] = useState<string>(initialText);
|
||||||
@@ -47,45 +44,43 @@ export default function useReactiveTextInput(
|
|||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(valueToSubmit: string) => {
|
(valueToSubmit: string) => {
|
||||||
// No need to update if it hasn't changed
|
// No need to update if it hasn't changed
|
||||||
if (valueToSubmit !== initialText) {
|
if (valueToSubmit === initialText) {
|
||||||
const cleanVal = valueToSubmit.trim();
|
return;
|
||||||
submitCallback(cleanVal);
|
}
|
||||||
if (cleanVal !== valueToSubmit) {
|
const cleanVal = valueToSubmit.trim();
|
||||||
setText(cleanVal);
|
submitCallback(cleanVal);
|
||||||
}
|
|
||||||
|
if (cleanVal !== valueToSubmit) {
|
||||||
|
setText(cleanVal);
|
||||||
}
|
}
|
||||||
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
|
||||||
},
|
},
|
||||||
[initialText, ref, submitCallback],
|
[initialText, submitCallback],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Handles escape events
|
* @description Handles common keys for submit and cancel
|
||||||
* @param {string} valueToSubmit
|
* @param {string} key
|
||||||
*/
|
*/
|
||||||
const handleEscape = useCallback(() => {
|
const keyHandler = useCallback(
|
||||||
// No need to update if it hasn't changed
|
(key: string) => {
|
||||||
setText(initialText);
|
switch (key) {
|
||||||
setTimeout(() => ref.current?.blur()); // Immediate timeout to ensure text is set before bluring
|
case 'Escape':
|
||||||
}, [initialText, ref]);
|
setText(initialText);
|
||||||
|
break;
|
||||||
const keyHandler = useMemo(() => {
|
case 'Enter':
|
||||||
const hotKeys: HotkeyItem[] = [['Escape', handleEscape, { preventDefault: true }]];
|
if (options?.submitOnEnter) {
|
||||||
|
handleSubmit(text);
|
||||||
if (options?.submitOnEnter) {
|
}
|
||||||
hotKeys.push(['Enter', () => handleSubmit(text)]);
|
break;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
if (options?.submitOnCtrlEnter) {
|
[initialText, options?.submitOnEnter, handleSubmit, text],
|
||||||
hotKeys.push(['mod + Enter', () => handleSubmit(text)]);
|
);
|
||||||
}
|
|
||||||
return getHotkeyHandler(hotKeys);
|
|
||||||
}, [handleEscape, handleSubmit, options?.submitOnCtrlEnter, options?.submitOnEnter, text]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: text,
|
value: text,
|
||||||
onChange: (event: ChangeEvent) => handleChange((event.target as HTMLInputElement).value),
|
onChange: (event: ChangeEvent) => handleChange((event.target as HTMLInputElement).value),
|
||||||
onBlur: (event: ChangeEvent) => handleSubmit((event.target as HTMLInputElement).value),
|
onBlur: (event: ChangeEvent) => handleSubmit((event.target as HTMLInputElement).value),
|
||||||
onKeyDown: keyHandler,
|
onKeyDown: (event: KeyboardEvent) => keyHandler(event.key),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ 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 percentRemaining = complete === 0 ? 0 : 100 - clamp(100 - (Math.max(now ?? 0, 0) * 100) / complete, 0, 100);
|
const percentComplete = 100 - clamp(100 - (Math.max(now ?? 0, 0) * 100) / complete, 0, 100);
|
||||||
|
|
||||||
const dangerWidth = danger ? clamp((danger / complete) * 100, 0, 100) : 0;
|
const dangerWidth = danger ? clamp((danger / complete) * 100, 0, 100) : 0;
|
||||||
const warningWidth = warning ? clamp((warning / complete) * 100, 0, 100) : 0;
|
const warningWidth = warning ? clamp((warning / complete) * 100, 0, 100) : 0;
|
||||||
@@ -37,7 +37,7 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
|||||||
className='multiprogress-bar__bg-danger'
|
className='multiprogress-bar__bg-danger'
|
||||||
style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }}
|
style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }}
|
||||||
/>
|
/>
|
||||||
<div className='multiprogress-bar__indicator' style={{ width: `${percentRemaining}%` }} />
|
<div className='multiprogress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
@use '../../../theme/viewerDefs' as *;
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
$progress-bar-size: 12px;
|
$progress-bar-size: 12px;
|
||||||
$progress-bar-br: 3px;
|
$progress-bar-br: 6px;
|
||||||
|
|
||||||
.progress-bar__bg {
|
.progress-bar__bg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: $progress-bar-size;
|
height: $progress-bar-size;
|
||||||
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);
|
||||||
overflow: clip;
|
|
||||||
|
|
||||||
&--hidden {
|
&--hidden {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -19,5 +18,6 @@ $progress-bar-br: 3px;
|
|||||||
height: $progress-bar-size;
|
height: $progress-bar-size;
|
||||||
background-color: var(--timer-progress-override, $accent-color);
|
background-color: var(--timer-progress-override, $accent-color);
|
||||||
transition: 1s linear;
|
transition: 1s linear;
|
||||||
|
border-radius: $progress-bar-br 0 0 $progress-bar-br;
|
||||||
transition-property: width;
|
transition-property: width;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ interface ScheduleProviderProps {
|
|||||||
time?: number;
|
time?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const numEventsPerPage = 8;
|
|
||||||
|
|
||||||
export const ScheduleProvider = ({
|
export const ScheduleProvider = ({
|
||||||
children,
|
children,
|
||||||
events,
|
events,
|
||||||
@@ -39,7 +37,7 @@ export const ScheduleProvider = ({
|
|||||||
// look for overrides from views
|
// look for overrides from views
|
||||||
const hidePast = isStringBoolean(searchParams.get('hidePast'));
|
const hidePast = isStringBoolean(searchParams.get('hidePast'));
|
||||||
const stopCycle = isStringBoolean(searchParams.get('stopCycle'));
|
const stopCycle = isStringBoolean(searchParams.get('stopCycle'));
|
||||||
const eventsPerPage = Number(searchParams.get('eventsPerPage') ?? numEventsPerPage);
|
const eventsPerPage = Number(searchParams.get('eventsPerPage') ?? 7);
|
||||||
|
|
||||||
let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
|
let selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, paramFields);
|
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, paramFields);
|
||||||
setSearchParams(newSearchParams);
|
setSearchParams(newSearchParams);
|
||||||
|
|
||||||
onClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -249,11 +249,12 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
|||||||
export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] => {
|
export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] => {
|
||||||
const topSourceOptions = makeOptionsFromCustomFields(customFields, {
|
const topSourceOptions = makeOptionsFromCustomFields(customFields, {
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
|
lowerMsg: 'Lower Third Message',
|
||||||
});
|
});
|
||||||
|
|
||||||
const bottomSourceOptions = makeOptionsFromCustomFields(customFields, {
|
const bottomSourceOptions = makeOptionsFromCustomFields(customFields, {
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
none: 'None',
|
lowerMsg: 'Lower Third Message',
|
||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -266,7 +267,7 @@ export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] =
|
|||||||
event: 'Event Load',
|
event: 'Event Load',
|
||||||
manual: 'Manual',
|
manual: 'Manual',
|
||||||
},
|
},
|
||||||
defaultValue: 'manual',
|
defaultValue: 'event',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'top-src',
|
id: 'top-src',
|
||||||
@@ -282,7 +283,7 @@ export const getLowerThirdOptions = (customFields: CustomFields): ParamField[] =
|
|||||||
description: 'Select the data source for the bottom element',
|
description: 'Select the data source for the bottom element',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: bottomSourceOptions,
|
values: bottomSourceOptions,
|
||||||
defaultValue: 'none',
|
defaultValue: 'lowerMsg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'top-colour',
|
id: 'top-colour',
|
||||||
@@ -393,9 +394,9 @@ export const getBackstageOptions = (timeFormat: string, customFields: CustomFiel
|
|||||||
{
|
{
|
||||||
id: 'eventsPerPage',
|
id: 'eventsPerPage',
|
||||||
title: 'Events per page',
|
title: 'Events per page',
|
||||||
description: 'Sets the number of events on the page, can cause overflow',
|
description: 'Sets the number of events on the page, can cause overlow',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
placeholder: '8 (default)',
|
placeholder: '7 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'secondary-src',
|
id: 'secondary-src',
|
||||||
@@ -430,9 +431,9 @@ export const getPublicOptions = (timeFormat: string, customFields: CustomFields)
|
|||||||
{
|
{
|
||||||
id: 'eventsPerPage',
|
id: 'eventsPerPage',
|
||||||
title: 'Events per page',
|
title: 'Events per page',
|
||||||
description: 'Sets the number of events on the page, can cause overflow',
|
description: 'Sets the number of events on the page, can cause overlow',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
placeholder: '8 (default)',
|
placeholder: '7 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'secondary-src',
|
id: 'secondary-src',
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||||
import {
|
import { getLinkedTimes, getPreviousEventNormal, reorderArray, swapEventData } from 'ontime-utils';
|
||||||
dayInMs,
|
|
||||||
getLinkedTimes,
|
|
||||||
getPreviousEventNormal,
|
|
||||||
MILLIS_PER_SECOND,
|
|
||||||
reorderArray,
|
|
||||||
swapEventData,
|
|
||||||
} from 'ontime-utils';
|
|
||||||
|
|
||||||
import { RUNDOWN } from '../api/constants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
import {
|
import {
|
||||||
@@ -28,11 +21,11 @@ import { useEditorSettings } from '../stores/editorSettings';
|
|||||||
import { forgivingStringToMillis } from '../utils/dateConfig';
|
import { forgivingStringToMillis } from '../utils/dateConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Set of utilities for events //TODO: should this be called useEntryAction and so on
|
* @description Set of utilities for events
|
||||||
*/
|
*/
|
||||||
export const useEventAction = () => {
|
export const useEventAction = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { defaultPublic, linkPrevious, defaultDuration, defaultWarnTime, defaultDangerTime } = useEditorSettings();
|
const { defaultPublic, linkPrevious, defaultDuration } = useEditorSettings((state) => state.eventSettings);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to add new event
|
* Calls mutation to add new event
|
||||||
@@ -55,8 +48,6 @@ export const useEventAction = () => {
|
|||||||
defaultPublic: boolean;
|
defaultPublic: boolean;
|
||||||
linkPrevious: boolean;
|
linkPrevious: boolean;
|
||||||
lastEventId: string;
|
lastEventId: string;
|
||||||
defaultWarnTime: number;
|
|
||||||
defaultDangerTime: number;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,8 +64,6 @@ export const useEventAction = () => {
|
|||||||
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
||||||
lastEventId: options?.lastEventId,
|
lastEventId: options?.lastEventId,
|
||||||
linkPrevious: options?.linkPrevious ?? linkPrevious,
|
linkPrevious: options?.linkPrevious ?? linkPrevious,
|
||||||
defaultWarnTime,
|
|
||||||
defaultDangerTime,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (applicationOptions.linkPrevious && applicationOptions?.lastEventId) {
|
if (applicationOptions.linkPrevious && applicationOptions?.lastEventId) {
|
||||||
@@ -96,14 +85,6 @@ export const useEventAction = () => {
|
|||||||
if (newEvent.duration === undefined && newEvent.timeEnd === undefined) {
|
if (newEvent.duration === undefined && newEvent.timeEnd === undefined) {
|
||||||
newEvent.duration = forgivingStringToMillis(defaultDuration);
|
newEvent.duration = forgivingStringToMillis(defaultDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newEvent.timeDanger === undefined) {
|
|
||||||
newEvent.timeDanger = forgivingStringToMillis(defaultDangerTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newEvent.timeWarning === undefined) {
|
|
||||||
newEvent.timeWarning = forgivingStringToMillis(defaultWarnTime);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle adding options that concern all event type
|
// handle adding options that concern all event type
|
||||||
@@ -117,7 +98,7 @@ export const useEventAction = () => {
|
|||||||
logAxiosError('Failed adding event', error);
|
logAxiosError('Failed adding event', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[_addEventMutation, defaultDangerTime, defaultDuration, defaultPublic, defaultWarnTime, linkPrevious, queryClient],
|
[_addEventMutation, defaultDuration, defaultPublic, linkPrevious],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,7 +155,7 @@ export const useEventAction = () => {
|
|||||||
|
|
||||||
const updateCustomField = useCallback(
|
const updateCustomField = useCallback(
|
||||||
async (eventId: string, field: string, value: string) => {
|
async (eventId: string, field: string, value: string) => {
|
||||||
updateEvent({ id: eventId, custom: { [field]: value } });
|
updateEvent({ id: eventId, custom: { [field]: { value } } });
|
||||||
},
|
},
|
||||||
[updateEvent],
|
[updateEvent],
|
||||||
);
|
);
|
||||||
@@ -222,12 +203,9 @@ export const useEventAction = () => {
|
|||||||
newValMillis = forgivingStringToMillis(value);
|
newValMillis = forgivingStringToMillis(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// dont allow timer values over 23:59:59
|
|
||||||
const cappedMillis = Math.min(newValMillis, dayInMs - MILLIS_PER_SECOND);
|
|
||||||
|
|
||||||
const newEvent = {
|
const newEvent = {
|
||||||
id: eventId,
|
id: eventId,
|
||||||
[field]: cappedMillis,
|
[field]: newValMillis,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await _updateEventMutation.mutateAsync(newEvent);
|
await _updateEventMutation.mutateAsync(newEvent);
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export const useOperator = () => {
|
|||||||
export const useMessageControl = () => {
|
export const useMessageControl = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
timer: state.message.timer,
|
timer: state.message.timer,
|
||||||
|
public: state.message.public,
|
||||||
|
lower: state.message.lower,
|
||||||
external: state.message.external,
|
external: state.message.external,
|
||||||
onAir: state.onAir,
|
onAir: state.onAir,
|
||||||
});
|
});
|
||||||
@@ -35,6 +37,10 @@ export const useMessageControl = () => {
|
|||||||
export const setMessage = {
|
export const setMessage = {
|
||||||
timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }),
|
timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }),
|
||||||
timerVisible: (payload: boolean) => socketSendJson('message', { timer: { visible: payload } }),
|
timerVisible: (payload: boolean) => socketSendJson('message', { timer: { visible: payload } }),
|
||||||
|
publicText: (payload: string) => socketSendJson('message', { public: { text: payload } }),
|
||||||
|
publicVisible: (payload: boolean) => socketSendJson('message', { public: { visible: payload } }),
|
||||||
|
lowerText: (payload: string) => socketSendJson('message', { lower: { text: payload } }),
|
||||||
|
lowerVisible: (payload: boolean) => socketSendJson('message', { lower: { visible: payload } }),
|
||||||
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
|
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
|
||||||
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
|
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
|
||||||
};
|
};
|
||||||
@@ -53,12 +59,12 @@ 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('load', 'previous');
|
socketSendJson('load', { previous: true });
|
||||||
},
|
},
|
||||||
next: () => {
|
next: () => {
|
||||||
socketSendJson('load', 'next');
|
socketSendJson('load', { next: true });
|
||||||
},
|
},
|
||||||
stop: () => {
|
stop: () => {
|
||||||
socketSendJson('stop');
|
socketSendJson('stop');
|
||||||
@@ -83,27 +89,27 @@ export const useInfoPanel = () => {
|
|||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAuxTimerTime = () => {
|
export const useExtraTimerTime = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => state.auxtimer1.current;
|
const featureSelector = (state: RuntimeStore) => state.timer1.current;
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAuxTimerControl = () => {
|
export const useExtraTimerControl = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
playback: state.auxtimer1.playback,
|
playback: state.timer1.playback,
|
||||||
direction: state.auxtimer1.direction,
|
direction: state.timer1.direction,
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAuxTimer = {
|
export const setExtraTimer = {
|
||||||
start: () => socketSendJson('auxtimer', { '1': SimplePlayback.Start }),
|
start: () => socketSendJson('extratimer', SimplePlayback.Start),
|
||||||
pause: () => socketSendJson('auxtimer', { '1': SimplePlayback.Pause }),
|
pause: () => socketSendJson('extratimer', SimplePlayback.Pause),
|
||||||
stop: () => socketSendJson('auxtimer', { '1': SimplePlayback.Stop }),
|
stop: () => socketSendJson('extratimer', SimplePlayback.Stop),
|
||||||
setDirection: (direction: SimpleDirection) => socketSendJson('auxtimer', { '1': { direction } }),
|
setDirection: (direction: SimpleDirection) => socketSendJson('extratimer', { direction }),
|
||||||
setDuration: (time: number) => socketSendJson('auxtimer', { '1': { duration: time } }),
|
setTime: (time: number) => socketSendJson('extratimer', { settime: time }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCuesheet = () => {
|
export const useCuesheet = () => {
|
||||||
|
|||||||
@@ -33,5 +33,5 @@ export const useAppMode = create<AppModeStore>()((set) => ({
|
|||||||
return { mode };
|
return { mode };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setCursor: (cursor: string | null) => set({ cursor }),
|
setCursor: (cursor: string | null) => set(() => ({ cursor })),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,68 +2,55 @@ import { create } from 'zustand';
|
|||||||
|
|
||||||
import { booleanFromLocalStorage } from '../utils/localStorage';
|
import { booleanFromLocalStorage } from '../utils/localStorage';
|
||||||
|
|
||||||
type EditorSettingsStore = {
|
type EditorSettings = {
|
||||||
defaultDuration: string;
|
|
||||||
linkPrevious: boolean;
|
linkPrevious: boolean;
|
||||||
defaultWarnTime: string;
|
|
||||||
defaultDangerTime: string;
|
|
||||||
defaultPublic: boolean;
|
defaultPublic: boolean;
|
||||||
setDefaultDuration: (defaultDuration: string) => void;
|
defaultDuration: string;
|
||||||
setLinkPrevious: (linkPrevious: boolean) => void;
|
|
||||||
setWarnTime: (warnTime: string) => void;
|
|
||||||
setDangerTime: (dangerTime: string) => void;
|
|
||||||
setDefaultPublic: (defaultPublic: boolean) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editorSettingsDefaults = {
|
type EditorSettingsStore = {
|
||||||
duration: '00:10:00',
|
eventSettings: EditorSettings;
|
||||||
linkPrevious: true,
|
setLocalEventSettings: (newState: EditorSettings) => void;
|
||||||
warnTime: '00:02:00', // 120000 same as backend
|
setLinkPrevious: (linkPrevious: boolean) => void;
|
||||||
dangerTime: '00:01:00', // 60000 same as backend
|
setDefaultPublic: (defaultPublic: boolean) => void;
|
||||||
isPublic: true,
|
setDefaultDuration: (defaultDuration: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum EditorSettingsKeys {
|
enum EditorSettingsKeys {
|
||||||
DefaultDuration = 'ontime-default-duration',
|
|
||||||
LinkPrevious = 'ontime-link-previous',
|
LinkPrevious = 'ontime-link-previous',
|
||||||
DefaultWarnTime = 'ontime-default-warn-time',
|
|
||||||
DefaultDangerTime = 'ontime-default-danger-time',
|
|
||||||
DefaultPublic = 'ontime-default-public',
|
DefaultPublic = 'ontime-default-public',
|
||||||
|
DefaultDuration = 'ontime-default-duration',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEditorSettings = create<EditorSettingsStore>((set) => {
|
export const useEditorSettings = create<EditorSettingsStore>((set) => ({
|
||||||
return {
|
eventSettings: {
|
||||||
defaultDuration: localStorage.getItem(EditorSettingsKeys.DefaultDuration) ?? editorSettingsDefaults.duration,
|
linkPrevious: booleanFromLocalStorage(EditorSettingsKeys.LinkPrevious, true),
|
||||||
linkPrevious: booleanFromLocalStorage(EditorSettingsKeys.LinkPrevious, editorSettingsDefaults.linkPrevious),
|
defaultPublic: booleanFromLocalStorage(EditorSettingsKeys.DefaultPublic, true),
|
||||||
defaultWarnTime: localStorage.getItem(EditorSettingsKeys.DefaultWarnTime) ?? editorSettingsDefaults.warnTime,
|
defaultDuration: localStorage.getItem(EditorSettingsKeys.DefaultDuration) ?? '00:10:00',
|
||||||
defaultDangerTime: localStorage.getItem(EditorSettingsKeys.DefaultDangerTime) ?? editorSettingsDefaults.dangerTime,
|
},
|
||||||
defaultPublic: booleanFromLocalStorage(EditorSettingsKeys.DefaultPublic, editorSettingsDefaults.isPublic),
|
|
||||||
|
|
||||||
setDefaultDuration: (defaultDuration) =>
|
setLocalEventSettings: (value) =>
|
||||||
set(() => {
|
set(() => {
|
||||||
localStorage.setItem(EditorSettingsKeys.DefaultDuration, String(defaultDuration));
|
localStorage.setItem(EditorSettingsKeys.LinkPrevious, String(value.linkPrevious));
|
||||||
return { defaultDuration };
|
localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(value.defaultPublic));
|
||||||
}),
|
return { eventSettings: value };
|
||||||
|
}),
|
||||||
|
|
||||||
setLinkPrevious: (linkPrevious) =>
|
setLinkPrevious: (linkPrevious) =>
|
||||||
set(() => {
|
set((state) => {
|
||||||
localStorage.setItem(EditorSettingsKeys.LinkPrevious, String(linkPrevious));
|
localStorage.setItem(EditorSettingsKeys.LinkPrevious, String(linkPrevious));
|
||||||
return { linkPrevious };
|
return { eventSettings: { ...state.eventSettings, linkPrevious } };
|
||||||
}),
|
}),
|
||||||
setWarnTime: (defaultWarnTime) =>
|
|
||||||
set(() => {
|
setDefaultPublic: (defaultPublic) =>
|
||||||
localStorage.setItem(EditorSettingsKeys.DefaultWarnTime, String(defaultWarnTime));
|
set((state) => {
|
||||||
return { defaultWarnTime };
|
localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(defaultPublic));
|
||||||
}),
|
return { eventSettings: { ...state.eventSettings, defaultPublic } };
|
||||||
setDangerTime: (defaultDangerTime) =>
|
}),
|
||||||
set(() => {
|
|
||||||
localStorage.setItem(EditorSettingsKeys.DefaultDangerTime, String(defaultDangerTime));
|
setDefaultDuration: (defaultDuration) =>
|
||||||
return { defaultDangerTime };
|
set((state) => {
|
||||||
}),
|
localStorage.setItem(EditorSettingsKeys.DefaultDuration, String(defaultDuration));
|
||||||
setDefaultPublic: (defaultPublic) =>
|
return { eventSettings: { ...state.eventSettings, defaultDuration } };
|
||||||
set(() => {
|
}),
|
||||||
localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(defaultPublic));
|
}));
|
||||||
return { defaultPublic };
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
|
|
||||||
type EntryCopyStore = {
|
|
||||||
entryCopyId: string | null;
|
|
||||||
setEntryCopyId: (eventId: string | null) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useEntryCopy = create<EntryCopyStore>()((set) => ({
|
|
||||||
entryCopyId: null,
|
|
||||||
setEntryCopyId: (entryCopyId: string | null) => set({ entryCopyId }),
|
|
||||||
}));
|
|
||||||
@@ -23,6 +23,14 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
|||||||
blink: false,
|
blink: false,
|
||||||
blackout: false,
|
blackout: false,
|
||||||
},
|
},
|
||||||
|
public: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
lower: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
external: {
|
external: {
|
||||||
text: '',
|
text: '',
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -41,7 +49,7 @@ export const runtimeStorePlaceholder: RuntimeStore = {
|
|||||||
eventNext: null,
|
eventNext: null,
|
||||||
publicEventNow: null,
|
publicEventNow: null,
|
||||||
publicEventNext: null,
|
publicEventNext: null,
|
||||||
auxtimer1: {
|
timer1: {
|
||||||
current: 0,
|
current: 0,
|
||||||
direction: SimpleDirection.CountUp,
|
direction: SimpleDirection.CountUp,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { forgivingStringToMillis, millisToDelayString } from '../dateConfig';
|
import { forgivingStringToMillis, millisToDelayString } from '../dateConfig';
|
||||||
|
|
||||||
describe('test forgivingStringToMillis()', () => {
|
describe('test forgivingStringToMillis()', () => {
|
||||||
@@ -8,22 +6,22 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
{ value: '', expect: 0 },
|
{ value: '', expect: 0 },
|
||||||
{ value: '0', expect: 0 },
|
{ value: '0', expect: 0 },
|
||||||
{ value: '-0', expect: 0 },
|
{ value: '-0', expect: 0 },
|
||||||
{ value: '1', expect: MILLIS_PER_MINUTE },
|
{ value: '1', expect: 60 * 1000 },
|
||||||
{ value: '-1', expect: MILLIS_PER_MINUTE },
|
{ value: '-1', expect: 60 * 1000 },
|
||||||
{ value: '0h0m0s', expect: 0 },
|
{ value: '0h0m0s', expect: 0 },
|
||||||
{ value: '0h0m1s', expect: MILLIS_PER_SECOND },
|
{ value: '0h0m1s', expect: 1000 },
|
||||||
{ value: '0h1m0s', expect: MILLIS_PER_MINUTE },
|
{ value: '0h1m0s', expect: 1000 * 60 },
|
||||||
{ value: '1h0m0s', expect: MILLIS_PER_HOUR },
|
{ value: '1h0m0s', expect: 1000 * 60 * 60 },
|
||||||
{ value: '23h0m0s', expect: 23 * MILLIS_PER_HOUR },
|
{ value: '23h0m0s', expect: 1000 * 60 * 60 * 23 },
|
||||||
{ value: '12h12m12s', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR },
|
{ value: '12h12m12s', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||||
{ value: '12H12M12S', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR },
|
{ value: '12H12M12S', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||||
{ value: '2m', expect: 2 * MILLIS_PER_MINUTE },
|
{ value: '2m', expect: 2 * 60 * 1000 },
|
||||||
{ value: '1h5s', expect: MILLIS_PER_HOUR + 5 * MILLIS_PER_SECOND },
|
{ value: '1h5s', expect: 1000 * 60 * 60 + 1000 * 5 },
|
||||||
{ value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
|
{ value: '1h2m', expect: 1000 * 60 * 60 + 1000 * 60 * 2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of testData) {
|
for (const s of testData) {
|
||||||
it(`handles ${s.value}`, () => {
|
it(`handles ${s.value} to left`, () => {
|
||||||
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
||||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||||
});
|
});
|
||||||
@@ -32,10 +30,10 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
|
|
||||||
describe('parses strings correctly', () => {
|
describe('parses strings correctly', () => {
|
||||||
const ts = [
|
const ts = [
|
||||||
{ value: '1.1.1', expect: MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND },
|
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||||
{ value: '12.1.1', expect: 12 * MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND },
|
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||||
{ value: '12.55.1', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + MILLIS_PER_SECOND },
|
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
|
||||||
{ value: '12.55.40', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + 40 * MILLIS_PER_SECOND },
|
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of ts) {
|
for (const s of ts) {
|
||||||
@@ -45,45 +43,17 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#33 separators are parsed according to doc examples ', () => {
|
|
||||||
const ts = [
|
|
||||||
{ value: '0.1', expect: MILLIS_PER_MINUTE },
|
|
||||||
{ value: '0 1', expect: MILLIS_PER_MINUTE },
|
|
||||||
{ value: '0:1', expect: MILLIS_PER_MINUTE },
|
|
||||||
{ value: '0,1', expect: MILLIS_PER_MINUTE },
|
|
||||||
{ value: '2.2.2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND },
|
|
||||||
{ value: '2 2 2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND },
|
|
||||||
{ value: '2:2:2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND },
|
|
||||||
{ value: '2,2,2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND },
|
|
||||||
{ value: '2,2,2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND },
|
|
||||||
{ value: '10:', expect: 10 * MILLIS_PER_HOUR },
|
|
||||||
{ value: ':10', expect: 10 * MILLIS_PER_MINUTE },
|
|
||||||
{ value: '10', expect: 10 * MILLIS_PER_MINUTE },
|
|
||||||
{ value: '120', expect: MILLIS_PER_HOUR + 20 * MILLIS_PER_MINUTE },
|
|
||||||
{ value: '90m', expect: 90 * MILLIS_PER_MINUTE },
|
|
||||||
{ value: '1.2', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
|
|
||||||
{ value: '1.2.3', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND },
|
|
||||||
{ value: '123456', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 56 * MILLIS_PER_SECOND },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const s of ts) {
|
|
||||||
it(`handles ${s.value}`, () => {
|
|
||||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parses time strings', () => {
|
describe('parses time strings', () => {
|
||||||
const ts = [
|
const ts = [
|
||||||
{ value: '1h2m3s', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND },
|
{ value: '1h2m3s', expect: 60 * 60 * 1000 + 2 * 60 * 1000 + 3 * 1000 },
|
||||||
{ value: '1h3s', expect: MILLIS_PER_HOUR + 3 * MILLIS_PER_SECOND },
|
{ value: '1h3s', expect: 60 * 60 * 1000 + 3 * 1000 },
|
||||||
{ value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
|
{ value: '1h2m', expect: 60 * 60 * 1000 + 2 * 60 * 1000 },
|
||||||
{ value: '10h', expect: 10 * MILLIS_PER_HOUR },
|
{ value: '10h', expect: 10 * 60 * 60 * 1000 },
|
||||||
{ value: '10m', expect: 10 * MILLIS_PER_MINUTE },
|
{ value: '10m', expect: 10 * 60 * 1000 },
|
||||||
{ value: '10s', expect: 10 * MILLIS_PER_SECOND },
|
{ value: '10s', expect: 10 * 1000 },
|
||||||
{ value: '120h', expect: 120 * MILLIS_PER_HOUR },
|
{ value: '120h', expect: 120 * 60 * 60 * 1000 },
|
||||||
{ value: '120m', expect: 120 * MILLIS_PER_MINUTE },
|
{ value: '120m', expect: 120 * 60 * 1000 },
|
||||||
{ value: '120s', expect: 120 * MILLIS_PER_SECOND },
|
{ value: '120s', expect: 120 * 1000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of ts) {
|
for (const s of ts) {
|
||||||
@@ -95,20 +65,20 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
|
|
||||||
describe('handles am/pm', () => {
|
describe('handles am/pm', () => {
|
||||||
const ampm = [
|
const ampm = [
|
||||||
{ value: '9:10:11am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
|
{ value: '9:10:11am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||||
{ value: '9:10:11a', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
|
{ value: '9:10:11a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||||
{ value: '9:10:11pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
|
{ value: '9:10:11pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||||
{ value: '9:10:11p', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 11 * MILLIS_PER_SECOND },
|
{ value: '9:10:11p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 },
|
||||||
{ value: '9:10am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
{ value: '9:10am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||||
{ value: '9:10a', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
{ value: '9:10a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||||
{ value: '9:10pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
{ value: '9:10pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||||
{ value: '9:10p', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
{ value: '9:10p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 },
|
||||||
{ value: '9am', expect: 9 * MILLIS_PER_HOUR },
|
{ value: '9am', expect: 9 * 60 * 60 * 1000 },
|
||||||
{ value: '9a', expect: 9 * MILLIS_PER_HOUR },
|
{ value: '9a', expect: 9 * 60 * 60 * 1000 },
|
||||||
{ value: '9pm', expect: (12 + 9) * MILLIS_PER_HOUR },
|
{ value: '9pm', expect: (12 + 9) * 60 * 60 * 1000 },
|
||||||
{ value: '9p', expect: (12 + 9) * MILLIS_PER_HOUR },
|
{ value: '9p', expect: (12 + 9) * 60 * 60 * 1000 },
|
||||||
{ value: '12am', expect: 0 },
|
{ value: '12am', expect: 0 },
|
||||||
{ value: '12pm', expect: 12 * MILLIS_PER_HOUR },
|
{ value: '12pm', expect: 12 * 60 * 60 * 1000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of ampm) {
|
for (const s of ampm) {
|
||||||
@@ -120,12 +90,12 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
|
|
||||||
describe('it infers separators when non existent', () => {
|
describe('it infers separators when non existent', () => {
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{ value: '1', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
{ value: '1', expect: 1000 * 60 }, // 00:01:00
|
||||||
{ value: '12', expect: 12 * MILLIS_PER_MINUTE }, // 00:12:00
|
{ value: '12', expect: 1000 * 60 * 12 }, // 00:12:00
|
||||||
{ value: '123', expect: MILLIS_PER_HOUR + 23 * MILLIS_PER_MINUTE }, // 01:23:00
|
{ value: '123', expect: 1000 * 60 * 23 + 1000 * 60 * 60 }, // 01:23:00
|
||||||
{ value: '1234', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE }, // 12:34:00
|
{ value: '1234', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 }, // 12:34:00
|
||||||
{ value: '12345', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 5 * MILLIS_PER_SECOND }, // 12:34:05
|
{ value: '12345', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 + 5 * 1000 }, // 12:34:05
|
||||||
{ value: '123456', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 56 * MILLIS_PER_SECOND }, // 12:34:56
|
{ value: '123456', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 + 56 * 1000 }, // 12:34:56
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of testCases) {
|
for (const s of testCases) {
|
||||||
@@ -135,12 +105,12 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sixDigits = [
|
const sixDigits = [
|
||||||
{ value: '000000', expect: 0 }, // 00:00:00
|
{ value: '000000', expect: 0 },
|
||||||
{ value: '000001', expect: MILLIS_PER_SECOND }, // 00:00:01
|
{ value: '000001', expect: 1000 },
|
||||||
{ value: '000100', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
{ value: '000100', expect: 1000 * 60 },
|
||||||
{ value: '010000', expect: MILLIS_PER_HOUR }, // 01:00:00
|
{ value: '010000', expect: 1000 * 60 * 60 },
|
||||||
{ value: '230000', expect: MILLIS_PER_HOUR * 23 }, // 23:00:00
|
{ value: '230000', expect: 1000 * 60 * 60 * 23 },
|
||||||
{ value: '121212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_SECOND }, // 12:12:12
|
{ value: '121212', expect: 12 * 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of sixDigits) {
|
for (const s of sixDigits) {
|
||||||
@@ -151,13 +121,13 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
|
|
||||||
const fiveDigits = [
|
const fiveDigits = [
|
||||||
{ value: '00000', expect: 0 },
|
{ value: '00000', expect: 0 },
|
||||||
{ value: '00001', expect: MILLIS_PER_SECOND }, // 00:00:01
|
{ value: '00001', expect: 1000 }, // 00:00:01
|
||||||
{ value: '00010', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
{ value: '00010', expect: 1000 * 60 }, // 00:01:00
|
||||||
{ value: '00100', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
|
{ value: '00100', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||||
{ value: '01000', expect: MILLIS_PER_HOUR }, // 01:00:00
|
{ value: '01000', expect: 1000 * 60 * 60 }, // 01:00:00
|
||||||
{ value: '10000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00
|
{ value: '10000', expect: 1000 * 60 * 60 * 10 }, // 10:00:00
|
||||||
{ value: '23000', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00
|
{ value: '23000', expect: 1000 * 60 * 60 * 23 }, // 23:00:00
|
||||||
{ value: '12121', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 1 * MILLIS_PER_SECOND }, // 12:12:01
|
{ value: '12121', expect: 1000 + 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, // 12:12:01
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of fiveDigits) {
|
for (const s of fiveDigits) {
|
||||||
@@ -168,12 +138,12 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
|
|
||||||
const fourDigits = [
|
const fourDigits = [
|
||||||
{ value: '0000', expect: 0 },
|
{ value: '0000', expect: 0 },
|
||||||
{ value: '0001', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
{ value: '0001', expect: 1000 * 60 }, // 00:01:00
|
||||||
{ value: '0010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
|
{ value: '0010', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||||
{ value: '0100', expect: MILLIS_PER_HOUR }, // 01:00:00
|
{ value: '0100', expect: 1000 * 60 * 60 }, // 01:00:00
|
||||||
{ value: '1000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00
|
{ value: '1000', expect: 1000 * 60 * 60 * 10 }, // 10:00:00
|
||||||
{ value: '2300', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00
|
{ value: '2300', expect: 1000 * 60 * 60 * 23 }, // 23:00:00
|
||||||
{ value: '1212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE }, // 12:12:00
|
{ value: '1212', expect: 12 * 60 * 1000 + 12 * 1000 * 60 * 60 }, // 12:12:00
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of fourDigits) {
|
for (const s of fourDigits) {
|
||||||
@@ -184,11 +154,11 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
|
|
||||||
const threeDigits = [
|
const threeDigits = [
|
||||||
{ value: '000', expect: 0 },
|
{ value: '000', expect: 0 },
|
||||||
{ value: '001', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
{ value: '001', expect: 1000 * 60 }, // 00:01:00
|
||||||
{ value: '010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
|
{ value: '010', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||||
{ value: '100', expect: MILLIS_PER_HOUR }, // 01:00:00
|
{ value: '100', expect: 1000 * 60 * 60 }, // 01:00:00
|
||||||
{ value: '230', expect: 2 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE }, // 02:30:00
|
{ value: '230', expect: 2 * 1000 * 60 * 60 + 30 * 1000 * 60 }, // 02:30:00
|
||||||
{ value: '121', expect: MILLIS_PER_HOUR + 21 * MILLIS_PER_MINUTE }, // 01:21:00
|
{ value: '121', expect: 21 * 60 * 1000 + 1000 * 60 * 60 }, // 01:21:00
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of threeDigits) {
|
for (const s of threeDigits) {
|
||||||
@@ -199,9 +169,9 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
|
|
||||||
const twoDigits = [
|
const twoDigits = [
|
||||||
{ value: '00', expect: 0 },
|
{ value: '00', expect: 0 },
|
||||||
{ value: '01', expect: MILLIS_PER_MINUTE }, // 00:01:00
|
{ value: '01', expect: 1000 * 60 }, // 00:01:00
|
||||||
{ value: '10', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00
|
{ value: '10', expect: 1000 * 60 * 10 }, // 00:10:00
|
||||||
{ value: '23', expect: 23 * MILLIS_PER_MINUTE }, // 00:23:00
|
{ value: '23', expect: 1000 * 60 * 23 }, // 00:23:00
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of twoDigits) {
|
for (const s of twoDigits) {
|
||||||
@@ -222,17 +192,18 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
describe('handles overflows', () => {
|
describe('handles overflows', () => {
|
||||||
const ts = [
|
const ts = [
|
||||||
// minutes overflow
|
// minutes overflow
|
||||||
{ value: '99', expect: 99 * MILLIS_PER_MINUTE },
|
{ value: '2.0.0', expect: 1000 * 60 * 120 },
|
||||||
{ value: '1.39.0', expect: 99 * MILLIS_PER_MINUTE },
|
{ value: '99', expect: 1000 * 60 * 99 },
|
||||||
|
{ value: '1.39.0', expect: 1000 * 60 * 99 },
|
||||||
// seconds overflow
|
// seconds overflow
|
||||||
{ value: '0.0.120', expect: 120 * MILLIS_PER_SECOND },
|
{ value: '0.0.120', expect: 120 * 1000 },
|
||||||
{ value: '0.2.0', expect: 120 * MILLIS_PER_SECOND },
|
{ value: '0.2.0', expect: 120 * 1000 },
|
||||||
{ value: '0.0.99', expect: 99 * MILLIS_PER_SECOND },
|
{ value: '0.0.99', expect: 99 * 1000 },
|
||||||
{ value: '0.1.39', expect: 99 * MILLIS_PER_SECOND },
|
{ value: '0.1.39', expect: 99 * 1000 },
|
||||||
// hours overflow
|
// hours overflow
|
||||||
{ value: '25.0.0', expect: 25 * MILLIS_PER_HOUR },
|
{ value: '25.0.0', expect: 1000 * 60 * 60 * 25 },
|
||||||
// hours overflow
|
// hours overflow
|
||||||
{ value: '50.0.0', expect: 50 * MILLIS_PER_HOUR },
|
{ value: '50.0.0', expect: 1000 * 60 * 60 * 50 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of ts) {
|
for (const s of ts) {
|
||||||
@@ -245,10 +216,10 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
describe('test fillLeft', () => {
|
describe('test fillLeft', () => {
|
||||||
describe('function handles separators', () => {
|
describe('function handles separators', () => {
|
||||||
const testData = [
|
const testData = [
|
||||||
{ value: '1:2:3:10', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND }, // 01:02:03
|
{ value: '1:2:3:10', expect: 3723000 },
|
||||||
{ value: '2,10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
{ value: '2,10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 },
|
||||||
{ value: '2.10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
{ value: '2.10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 },
|
||||||
{ value: '2 10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE },
|
{ value: '2 10', expect: 2 * 60 * 60 * 1000 + 60 * 10 * 1000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of testData) {
|
for (const s of testData) {
|
||||||
@@ -261,8 +232,8 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
|
|
||||||
describe('parses strings correctly', () => {
|
describe('parses strings correctly', () => {
|
||||||
const ts = [
|
const ts = [
|
||||||
{ value: '1.2', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE },
|
{ value: '1.2', expect: 60 * 60 * 1000 + 2 * 60 * 1000 },
|
||||||
{ value: '1.70', expect: MILLIS_PER_HOUR + 70 * MILLIS_PER_MINUTE },
|
{ value: '1.70', expect: 60 * 60 * 1000 + 70 * 60 * 1000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of ts) {
|
for (const s of ts) {
|
||||||
@@ -275,8 +246,8 @@ describe('test forgivingStringToMillis()', () => {
|
|||||||
describe('handles overflows', () => {
|
describe('handles overflows', () => {
|
||||||
const ts = [
|
const ts = [
|
||||||
// minutes overflow
|
// minutes overflow
|
||||||
{ value: '0.120', expect: 120 * MILLIS_PER_MINUTE },
|
{ value: '0.120', expect: 120 * 60 * 1000 },
|
||||||
{ value: '0.99', expect: 99 * MILLIS_PER_MINUTE },
|
{ value: '0.99', expect: 99 * 60 * 1000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const s of ts) {
|
for (const s of ts) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('cloneEvent()', () => {
|
|||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
custom: {
|
custom: {
|
||||||
lighting: '3',
|
lighting: { value: '3' },
|
||||||
} as EventCustomFields,
|
} as EventCustomFields,
|
||||||
} as OntimeEvent;
|
} as OntimeEvent;
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,12 @@ function inferSeparators(value: string, isAM: boolean, isPM: boolean) {
|
|||||||
inferredMillis = parse(value[0]) * MILLIS_PER_HOUR + parse(value.substring(1)) * MILLIS_PER_MINUTE;
|
inferredMillis = parse(value[0]) * MILLIS_PER_HOUR + parse(value.substring(1)) * MILLIS_PER_MINUTE;
|
||||||
} else if (length === 4) {
|
} else if (length === 4) {
|
||||||
inferredMillis = parse(value.substring(0, 2)) * MILLIS_PER_HOUR + parse(value.substring(2)) * MILLIS_PER_MINUTE;
|
inferredMillis = parse(value.substring(0, 2)) * MILLIS_PER_HOUR + parse(value.substring(2)) * MILLIS_PER_MINUTE;
|
||||||
} else if (length >= 5) {
|
} else if (length === 5) {
|
||||||
|
const hours = parse(value.substring(0, 2));
|
||||||
|
const minutes = parse(value.substring(2, 4));
|
||||||
|
const seconds = parse(value.substring(4));
|
||||||
|
inferredMillis = hours * MILLIS_PER_HOUR + minutes * MILLIS_PER_MINUTE + seconds * MILLIS_PER_SECOND;
|
||||||
|
} else if (length >= 6) {
|
||||||
const hours = parse(value.substring(0, 2));
|
const hours = parse(value.substring(0, 2));
|
||||||
const minutes = parse(value.substring(2, 4));
|
const minutes = parse(value.substring(2, 4));
|
||||||
const seconds = parse(value.substring(4));
|
const seconds = parse(value.substring(4));
|
||||||
@@ -142,7 +147,6 @@ export const forgivingStringToMillis = (value: string): number => {
|
|||||||
hoursMatchValue = addAM;
|
hoursMatchValue = addAM;
|
||||||
}
|
}
|
||||||
if (first != null && second != null && third == null) {
|
if (first != null && second != null && third == null) {
|
||||||
// if string has two sections, treat as [hours] [minutes]
|
|
||||||
millis = parse(first) * MILLIS_PER_HOUR;
|
millis = parse(first) * MILLIS_PER_HOUR;
|
||||||
millis += parse(second) * MILLIS_PER_MINUTE;
|
millis += parse(second) * MILLIS_PER_MINUTE;
|
||||||
}
|
}
|
||||||
@@ -151,7 +155,6 @@ export const forgivingStringToMillis = (value: string): number => {
|
|||||||
if (isPM && hoursMatchValue < 12) {
|
if (isPM && hoursMatchValue < 12) {
|
||||||
millis += 12 * MILLIS_PER_HOUR;
|
millis += 12 * MILLIS_PER_HOUR;
|
||||||
}
|
}
|
||||||
|
|
||||||
return millis;
|
return millis;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,3 @@ export function isMacOS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const deviceAlt = isMacOS() ? '⌥' : 'Alt';
|
export const deviceAlt = isMacOS() ? '⌥' : 'Alt';
|
||||||
|
|
||||||
export const deviceMod = isMacOS() ? '⌘' : 'Ctrl';
|
|
||||||
|
|||||||
@@ -8,5 +8,4 @@ export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
|||||||
export const startsWithHttp = /^http:\/\//;
|
export const startsWithHttp = /^http:\/\//;
|
||||||
export const startsWithSlash = /^\//;
|
export const startsWithSlash = /^\//;
|
||||||
export const isAlphanumeric = /^[a-z0-9]+$/i;
|
export const isAlphanumeric = /^[a-z0-9]+$/i;
|
||||||
export const isASCII = /^[ -~]+$/; //https://catonmat.net/my-favorite-regex
|
|
||||||
export const isNotEmpty = /\S/;
|
export const isNotEmpty = /\S/;
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ export const connectSocket = (preferredClientName?: string) => {
|
|||||||
updateDevTools({ publicEventNext: payload });
|
updateDevTools({ publicEventNext: payload });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ontime-auxtimer1': {
|
case 'ontime-timer1': {
|
||||||
patchRuntime('auxtimer1', payload);
|
patchRuntime('timer1', payload);
|
||||||
updateDevTools({ auxtimer1: payload });
|
updateDevTools({ timer1: payload });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,3 @@ export const cx = (classNames: any[]) => classNames.filter(Boolean).join(' ');
|
|||||||
export const enDash = '–';
|
export const enDash = '–';
|
||||||
|
|
||||||
export const timerPlaceholder = '––:––:––';
|
export const timerPlaceholder = '––:––:––';
|
||||||
export const timerPlaceholderMin = '––:––';
|
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: $ui-black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { ErrorBoundary } from '@sentry/react';
|
|||||||
import { useKeyDown } from '../../common/hooks/useKeyDown';
|
import { useKeyDown } from '../../common/hooks/useKeyDown';
|
||||||
|
|
||||||
import AboutPanel from './panel/about-panel/AboutPanel';
|
import AboutPanel from './panel/about-panel/AboutPanel';
|
||||||
import FeatureSettingsPanel from './panel/feature-settings-panel/FeatureSettingsPanel';
|
|
||||||
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
||||||
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
||||||
|
import InterfacePanel from './panel/interface-panel/InterfacePanel';
|
||||||
import LogPanel from './panel/log-panel/LogPanel';
|
import LogPanel from './panel/log-panel/LogPanel';
|
||||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||||
|
import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel';
|
||||||
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
||||||
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
||||||
import PanelContent from './panel-content/PanelContent';
|
import PanelContent from './panel-content/PanelContent';
|
||||||
@@ -27,8 +28,9 @@ export default function AppSettings() {
|
|||||||
<PanelContent onClose={close}>
|
<PanelContent onClose={close}>
|
||||||
{panel === 'project' && <ProjectPanel location={location} />}
|
{panel === 'project' && <ProjectPanel location={location} />}
|
||||||
{panel === 'general' && <GeneralPanel location={location} />}
|
{panel === 'general' && <GeneralPanel location={location} />}
|
||||||
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
|
{panel === 'project_settings' && <ProjectSettingsPanel />}
|
||||||
{panel === 'sources' && <SourcesPanel />}
|
{panel === 'sources' && <SourcesPanel />}
|
||||||
|
{panel === 'interface' && <InterfacePanel />}
|
||||||
{panel === 'integrations' && <IntegrationsPanel location={location} />}
|
{panel === 'integrations' && <IntegrationsPanel location={location} />}
|
||||||
{panel === 'about' && <AboutPanel />}
|
{panel === 'about' && <AboutPanel />}
|
||||||
{panel === 'log' && <LogPanel />}
|
{panel === 'log' && <LogPanel />}
|
||||||
|
|||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
|
||||||
import * as Panel from '../PanelUtils';
|
|
||||||
|
|
||||||
import CustomFields from './custom-fields/CustomFields';
|
|
||||||
import UrlPresetsForm from './UrlPresetsForm';
|
|
||||||
|
|
||||||
export default function FeatureSettingsPanel({ location }: PanelBaseProps) {
|
|
||||||
const customFieldsRef = useScrollIntoView<HTMLDivElement>('custom', location);
|
|
||||||
const urlPresetsRef = useScrollIntoView<HTMLDivElement>('urlpresets', location);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Panel.Header>Feature Settings</Panel.Header>
|
|
||||||
<div ref={customFieldsRef}>
|
|
||||||
<CustomFields />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ref={urlPresetsRef}>
|
|
||||||
<UrlPresetsForm />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-98
@@ -1,98 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Alert, AlertDescription, AlertIcon, Button } from '@chakra-ui/react';
|
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
|
||||||
import { CustomField, CustomFieldLabel } from 'ontime-types';
|
|
||||||
|
|
||||||
import { deleteCustomField, editCustomField, postCustomField } from '../../../../../common/api/customFields';
|
|
||||||
import ExternalLink from '../../../../../common/components/external-link/ExternalLink';
|
|
||||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
|
||||||
import * as Panel from '../../PanelUtils';
|
|
||||||
|
|
||||||
import CustomFieldEntry from './CustomFieldEntry';
|
|
||||||
import CustomFieldForm from './CustomFieldForm';
|
|
||||||
|
|
||||||
const customFieldsDocsUrl = 'https://docs.getontime.no/features/custom-fields/';
|
|
||||||
|
|
||||||
export default function CustomFields() {
|
|
||||||
const { data, refetch } = useCustomFields();
|
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
|
||||||
|
|
||||||
const handleInitiateCreate = () => {
|
|
||||||
setIsAdding(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setIsAdding(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreate = async (customField: CustomField) => {
|
|
||||||
await postCustomField(customField);
|
|
||||||
refetch();
|
|
||||||
setIsAdding(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditField = async (label: CustomFieldLabel, customField: CustomField) => {
|
|
||||||
await editCustomField(label, customField);
|
|
||||||
refetch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (label: string) => {
|
|
||||||
try {
|
|
||||||
await deleteCustomField(label);
|
|
||||||
refetch();
|
|
||||||
} catch (_error) {
|
|
||||||
/** we do not handle errors here */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Panel.Section>
|
|
||||||
<Panel.Card>
|
|
||||||
<Panel.SubHeader>
|
|
||||||
Custom fields
|
|
||||||
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleInitiateCreate}>
|
|
||||||
New
|
|
||||||
</Button>
|
|
||||||
</Panel.SubHeader>
|
|
||||||
<Panel.Divider />
|
|
||||||
<Panel.Section>
|
|
||||||
<Alert status='info' variant='ontime-on-dark-info'>
|
|
||||||
<AlertIcon />
|
|
||||||
<AlertDescription>
|
|
||||||
Custom fields allow for additional information to be added to an event (eg. light, sound, camera). <br />
|
|
||||||
<br />
|
|
||||||
This data is not used by Ontime.
|
|
||||||
<ExternalLink href={customFieldsDocsUrl}>See the docs</ExternalLink>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</Panel.Section>
|
|
||||||
<Panel.Section>
|
|
||||||
{isAdding && <CustomFieldForm onSubmit={handleCreate} onCancel={handleCancel} />}
|
|
||||||
<Panel.Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Colour</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Object.entries(data).map(([key, { colour, label }]) => {
|
|
||||||
return (
|
|
||||||
<CustomFieldEntry
|
|
||||||
key={key}
|
|
||||||
field={key}
|
|
||||||
colour={colour}
|
|
||||||
label={label}
|
|
||||||
onEdit={handleEditField}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Panel.Table>
|
|
||||||
</Panel.Section>
|
|
||||||
</Panel.Card>
|
|
||||||
</Panel.Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,3 +2,23 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pad {
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fit {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aliasConstrain {
|
||||||
|
min-width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullWidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import { PanelBaseProps } from '../../settingsStore';
|
||||||
import EditorSettingsForm from '../interface-panel/EditorSettingsForm';
|
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import GeneralPanelForm from './GeneralPanelForm';
|
import GeneralPanelForm from './GeneralPanelForm';
|
||||||
|
import UrlPresetsForm from './UrlPresetsForm';
|
||||||
import ViewSettingsForm from './ViewSettingsForm';
|
import ViewSettingsForm from './ViewSettingsForm';
|
||||||
|
|
||||||
export default function GeneralPanel({ location }: PanelBaseProps) {
|
export default function GeneralPanel({ location }: PanelBaseProps) {
|
||||||
const generalRef = useScrollIntoView<HTMLDivElement>('settings', location);
|
const manageRef = useScrollIntoView<HTMLDivElement>('manage', location);
|
||||||
const editorRef = useScrollIntoView<HTMLDivElement>('editor', location);
|
|
||||||
const viewRef = useScrollIntoView<HTMLDivElement>('view', location);
|
const viewRef = useScrollIntoView<HTMLDivElement>('view', location);
|
||||||
|
const urlPresetsRef = useScrollIntoView<HTMLDivElement>('urlpresets', location);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>App Settings</Panel.Header>
|
<Panel.Header>App Settings</Panel.Header>
|
||||||
<div ref={generalRef}>
|
<div ref={manageRef}>
|
||||||
<GeneralPanelForm />
|
<GeneralPanelForm />
|
||||||
</div>
|
</div>
|
||||||
<div ref={editorRef}>
|
|
||||||
<EditorSettingsForm />
|
|
||||||
</div>
|
|
||||||
<div ref={viewRef}>
|
<div ref={viewRef}>
|
||||||
<ViewSettingsForm />
|
<ViewSettingsForm />
|
||||||
</div>
|
</div>
|
||||||
|
<div ref={urlPresetsRef}>
|
||||||
|
<UrlPresetsForm />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -15,7 +15,7 @@ import { handleLinks } from '../../../../common/utils/linkUtils';
|
|||||||
import { validateUrlPresetPath } from '../../../../common/utils/urlPresets';
|
import { validateUrlPresetPath } from '../../../../common/utils/urlPresets';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import style from './FeatureSettings.module.scss';
|
import style from './GeneralPanel.module.scss';
|
||||||
|
|
||||||
const urlPresetsDocs = 'https://docs.getontime.no/features/url-presets/';
|
const urlPresetsDocs = 'https://docs.getontime.no/features/url-presets/';
|
||||||
|
|
||||||
@@ -91,12 +91,12 @@ export default function UrlPresetsForm() {
|
|||||||
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} data-testid='url-preset-form'>
|
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)} data-testid='url-preset-form'>
|
||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
URL presets
|
URL Presets
|
||||||
<div className={style.actionButtons}>
|
<div className={style.actionButtons}>
|
||||||
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={!canSubmit}>
|
<Button variant='ontime-ghosted' size='md' onClick={onReset} isDisabled={!canSubmit}>
|
||||||
Revert to saved
|
Revert to saved
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='ontime-filled' size='sm' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}>
|
<Button variant='ontime-filled' size='md' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Alert, AlertDescription, AlertIcon, Button, Input, Switch } from '@chakra-ui/react';
|
import { Alert, AlertDescription, AlertIcon, Button, Input, Switch } from '@chakra-ui/react';
|
||||||
import { ViewSettings } from 'ontime-types';
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
@@ -99,13 +99,7 @@ export default function ViewSettingsForm() {
|
|||||||
title='Override CSS styles'
|
title='Override CSS styles'
|
||||||
description='Enables overriding view styles with custom stylesheet'
|
description='Enables overriding view styles with custom stylesheet'
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Switch {...register('overrideStyles')} variant='ontime' size='lg' />
|
||||||
control={control}
|
|
||||||
name='overrideStyles'
|
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
@@ -126,15 +120,9 @@ export default function ViewSettingsForm() {
|
|||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='Freeze timer on end'
|
title='Freeze timer on end'
|
||||||
description='Timer in views will stop from going negative and instead stop at 00:00:00'
|
description='Timer in views will stop from going negative after reaching'
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='freezeEnd'
|
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
<Switch {...register('freezeEnd')} variant='ontime' size='lg' />
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { Button, IconButton, Input, Select, Switch } from '@chakra-ui/react';
|
import { Button, IconButton, Input, Select, Switch } from '@chakra-ui/react';
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||||
@@ -75,7 +75,7 @@ export default function HttpIntegrations() {
|
|||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
HTTP settings
|
HTTP
|
||||||
<div className={style.flex}>
|
<div className={style.flex}>
|
||||||
<Button variant='ontime-ghosted' size='sm' onClick={() => reset()} isDisabled={!canSubmit}>
|
<Button variant='ontime-ghosted' size='sm' onClick={() => reset()} isDisabled={!canSubmit}>
|
||||||
Revert to saved
|
Revert to saved
|
||||||
@@ -99,13 +99,7 @@ export default function HttpIntegrations() {
|
|||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='HTTP Output' description='Provide feedback from Ontime through HTTP' />
|
<Panel.Field title='HTTP Output' description='Provide feedback from Ontime through HTTP' />
|
||||||
<Controller
|
<Switch variant='ontime' size='lg' {...register('enabledOut')} />
|
||||||
control={control}
|
|
||||||
name='enabledOut'
|
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
|
|
||||||
|
|||||||
-4
@@ -2,10 +2,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.halfWidth {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fitContents.fitContents {
|
.fitContents.fitContents {
|
||||||
width: max-content; /* override chakra */
|
width: max-content; /* override chakra */
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-50
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { Button, IconButton, Input, Select, Switch } from '@chakra-ui/react';
|
import { Button, IconButton, Input, Select, Switch } from '@chakra-ui/react';
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||||
@@ -8,7 +8,7 @@ import { generateId } from 'ontime-utils';
|
|||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
||||||
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { isASCII, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
import { isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import { cycles } from './integrationUtils';
|
import { cycles } from './integrationUtils';
|
||||||
@@ -65,8 +65,7 @@ export default function OscIntegrations() {
|
|||||||
prepend({
|
prepend({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
cycle: 'onLoad',
|
cycle: 'onLoad',
|
||||||
address: '',
|
message: '',
|
||||||
payload: '',
|
|
||||||
enabled: false,
|
enabled: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -81,7 +80,7 @@ export default function OscIntegrations() {
|
|||||||
return (
|
return (
|
||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
OSC settings
|
Open Sound Control
|
||||||
<div className={style.flex}>
|
<div className={style.flex}>
|
||||||
<Button variant='ontime-ghosted' size='sm' onClick={() => reset()} isDisabled={!canSubmit}>
|
<Button variant='ontime-ghosted' size='sm' onClick={() => reset()} isDisabled={!canSubmit}>
|
||||||
Revert to saved
|
Revert to saved
|
||||||
@@ -103,18 +102,12 @@ export default function OscIntegrations() {
|
|||||||
|
|
||||||
<Panel.Section as='form' id='osc-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
|
<Panel.Section as='form' id='osc-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
|
||||||
<Panel.Loader isLoading={isLoading} />
|
<Panel.Loader isLoading={isLoading} />
|
||||||
<Panel.Title>General OSC settings</Panel.Title>
|
<Panel.Title>OSC Settings</Panel.Title>
|
||||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='OSC input' description='Allow control of Ontime through OSC' />
|
<Panel.Field title='OSC input' description='Allow control of Ontime through OSC' />
|
||||||
<Controller
|
<Switch variant='ontime' size='lg' {...register('enabledIn')} />
|
||||||
control={control}
|
|
||||||
name='enabledIn'
|
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
@@ -147,13 +140,7 @@ export default function OscIntegrations() {
|
|||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='OSC output' description='Provide feedback from Ontime with OSC' />
|
<Panel.Field title='OSC output' description='Provide feedback from Ontime with OSC' />
|
||||||
<Controller
|
<Switch variant='ontime' size='lg' {...register('enabledOut')} />
|
||||||
control={control}
|
|
||||||
name='enabledOut'
|
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
|
||||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
@@ -208,7 +195,7 @@ export default function OscIntegrations() {
|
|||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
|
|
||||||
<Panel.Title>
|
<Panel.Title>
|
||||||
OSC integrations
|
OSC Integrations
|
||||||
<Button variant='ontime-subtle' size='sm' rightIcon={<IoAdd />} onClick={handleAddNewSubscription}>
|
<Button variant='ontime-subtle' size='sm' rightIcon={<IoAdd />} onClick={handleAddNewSubscription}>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
@@ -220,15 +207,14 @@ export default function OscIntegrations() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Enabled</th>
|
<th>Enabled</th>
|
||||||
<th>Cycle</th>
|
<th>Cycle</th>
|
||||||
<th className={style.halfWidth}>Address</th>
|
<th className={style.fullWidth}>Message</th>
|
||||||
<th className={style.halfWidth}>Payload</th>
|
|
||||||
<th />
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
const maybeAddressError = errors.subscriptions?.[index]?.address?.message;
|
// @ts-expect-error -- not sure why it is not finding the type, it is ok
|
||||||
const maybePayloadError = errors.subscriptions?.[index]?.payload?.message;
|
const maybeError = errors.subscriptions?.[index]?.message?.message;
|
||||||
return (
|
return (
|
||||||
<tr key={field.id}>
|
<tr key={field.id}>
|
||||||
<td>
|
<td>
|
||||||
@@ -248,40 +234,22 @@ export default function OscIntegrations() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</td>
|
</td>
|
||||||
<td className={style.halfWidth}>
|
<td className={style.fullWidth}>
|
||||||
<Input
|
<Input
|
||||||
key={field.id}
|
key={field.id}
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
placeholder='/from-ontime/'
|
placeholder='/from-ontime/{{timer.current}}'
|
||||||
{...register(`subscriptions.${index}.address`, {
|
{...register(`subscriptions.${index}.message`, {
|
||||||
required: { value: true, message: 'Required field' },
|
required: { value: true, message: 'Required field' },
|
||||||
validate: {
|
pattern: {
|
||||||
oscStartsWithSlash: (value) =>
|
value: startsWithSlash,
|
||||||
startsWithSlash.test(value) || 'OSC address should start with a forward slash',
|
message: 'OSC messages should start with a forward slash',
|
||||||
oscStringIsAscii: (value) =>
|
|
||||||
isASCII.test(value) || 'OSC address only allow ASCII characters',
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{maybeAddressError && <Panel.Error>{maybeAddressError}</Panel.Error>}
|
{maybeError && <Panel.Error>{maybeError}</Panel.Error>}
|
||||||
</td>
|
|
||||||
<td className={style.halfWidth}>
|
|
||||||
<Input
|
|
||||||
key={field.id}
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled'
|
|
||||||
autoComplete='off'
|
|
||||||
placeholder='{{timer.current}}'
|
|
||||||
{...register(`subscriptions.${index}.payload`, {
|
|
||||||
validate: {
|
|
||||||
oscStringIsAscii: (value) =>
|
|
||||||
isASCII.test(value) || 'OSC payloads only allow ASCII characters',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{maybePayloadError && <Panel.Error>{maybePayloadError}</Panel.Error>}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
+13
-56
@@ -1,23 +1,17 @@
|
|||||||
import { Select, Switch } from '@chakra-ui/react';
|
import { Switch } from '@chakra-ui/react';
|
||||||
import { EndAction, TimerType } from 'ontime-types';
|
|
||||||
|
|
||||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||||
import { editorSettingsDefaults, useEditorSettings } from '../../../../common/stores/editorSettings';
|
import { useEditorSettings } from '../../../../common/stores/editorSettings';
|
||||||
import { forgivingStringToMillis } from '../../../../common/utils/dateConfig';
|
import { forgivingStringToMillis } from '../../../../common/utils/dateConfig';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
export default function EditorSettingsForm() {
|
export default function EditorSettingsForm() {
|
||||||
const eventSettings = useEditorSettings((state) => state);
|
const eventSettings = useEditorSettings((state) => state.eventSettings);
|
||||||
|
const setLinkPrevious = useEditorSettings((state) => state.setLinkPrevious);
|
||||||
const setDefaultDuration = eventSettings.setDefaultDuration;
|
const setDefaultPublic = useEditorSettings((state) => state.setDefaultPublic);
|
||||||
const setLinkPrevious = eventSettings.setLinkPrevious;
|
const setDefaultDuration = useEditorSettings((state) => state.setDefaultDuration);
|
||||||
const setWarnTime = eventSettings.setWarnTime;
|
|
||||||
const setDangerTime = eventSettings.setDangerTime;
|
|
||||||
const setDefaultPublic = eventSettings.setDefaultPublic;
|
|
||||||
|
|
||||||
const durationInMs = forgivingStringToMillis(eventSettings.defaultDuration);
|
const durationInMs = forgivingStringToMillis(eventSettings.defaultDuration);
|
||||||
const warnTimeInMs = forgivingStringToMillis(eventSettings.defaultWarnTime);
|
|
||||||
const dangerTimeInMs = forgivingStringToMillis(eventSettings.defaultDangerTime);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
@@ -25,21 +19,24 @@ export default function EditorSettingsForm() {
|
|||||||
<Panel.SubHeader>Editor settings</Panel.SubHeader>
|
<Panel.SubHeader>Editor settings</Panel.SubHeader>
|
||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Title>Rundown defaults for new events</Panel.Title>
|
<Panel.Title>Rundown options</Panel.Title>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Default duration' description='Default duration for new events' />
|
<Panel.Field
|
||||||
|
title='Default duration'
|
||||||
|
description='When creating a new event, what is the default duration'
|
||||||
|
/>
|
||||||
<TimeInput<'defaultDuration'>
|
<TimeInput<'defaultDuration'>
|
||||||
name='defaultDuration'
|
name='defaultDuration'
|
||||||
submitHandler={(_field, value) => setDefaultDuration(value)}
|
submitHandler={(_field, value) => setDefaultDuration(value)}
|
||||||
time={durationInMs}
|
time={durationInMs}
|
||||||
placeholder={editorSettingsDefaults.duration}
|
placeholder='00:10:00'
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='Link previous'
|
title='Link previous'
|
||||||
description='Whether the start time of new events should be linked to the previous event end time'
|
description='New events start time will be linked to the previous event'
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
variant='ontime'
|
variant='ontime'
|
||||||
@@ -48,46 +45,6 @@ export default function EditorSettingsForm() {
|
|||||||
onChange={(event) => setLinkPrevious(event.target.checked)}
|
onChange={(event) => setLinkPrevious(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field title='Timer type' description='Default type of timer for new events' />
|
|
||||||
<Select variant='ontime' size='sm' width='auto' isDisabled>
|
|
||||||
<option value={TimerType.CountDown}>Count down</option>
|
|
||||||
<option value={TimerType.CountUp}>Count up</option>
|
|
||||||
<option value={TimerType.TimeToEnd}>Time to end</option>
|
|
||||||
<option value={TimerType.Clock}>Clock</option>
|
|
||||||
</Select>
|
|
||||||
</Panel.ListItem>
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field title='End Action' description='Default end action for new events' />
|
|
||||||
<Select variant='ontime' size='sm' width='auto' isDisabled>
|
|
||||||
<option value={EndAction.None}>None</option>
|
|
||||||
<option value={EndAction.Stop}>Stop</option>
|
|
||||||
<option value={EndAction.LoadNext}>Load next</option>
|
|
||||||
<option value={EndAction.PlayNext}>Play next</option>
|
|
||||||
</Select>
|
|
||||||
</Panel.ListItem>
|
|
||||||
</Panel.ListGroup>
|
|
||||||
<Panel.ListGroup>
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field title='Warning time' description='Default threshold for warning time in an event' />
|
|
||||||
<TimeInput<'warnTime'>
|
|
||||||
name='warnTime'
|
|
||||||
submitHandler={(_field, value) => setWarnTime(value)}
|
|
||||||
time={warnTimeInMs}
|
|
||||||
placeholder={editorSettingsDefaults.warnTime}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
|
||||||
<Panel.ListItem>
|
|
||||||
<Panel.Field title='Danger time' description='Default threshold for danger time in an event' />
|
|
||||||
<TimeInput<'dangerTime'>
|
|
||||||
name='dangerTime'
|
|
||||||
submitHandler={(_field, value) => setDangerTime(value)}
|
|
||||||
time={dangerTimeInMs}
|
|
||||||
placeholder={editorSettingsDefaults.dangerTime}
|
|
||||||
/>
|
|
||||||
</Panel.ListItem>
|
|
||||||
</Panel.ListGroup>
|
|
||||||
<Panel.ListGroup>
|
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field title='Default public' description='New events will be public' />
|
<Panel.Field title='Default public' description='New events will be public' />
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import InfoNif from './NetworkInterfaces';
|
|||||||
export default function LogPanel() {
|
export default function LogPanel() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Log</Panel.Header>
|
<Panel.Header>Application log</Panel.Header>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
|
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function ProjectData() {
|
|||||||
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)}>
|
<Panel.Section as='form' onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
Project data
|
Project Data
|
||||||
<div className={style.headerButtons}>
|
<div className={style.headerButtons}>
|
||||||
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={isSubmitting || !isDirty}>
|
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={isSubmitting || !isDirty}>
|
||||||
Revert to saved
|
Revert to saved
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ export default function ProjectList() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const reorderedProjectFiles = useMemo(() => {
|
const reorderedProjectFiles = useMemo(() => {
|
||||||
if (!data.files?.length) return [];
|
if (!data?.files?.length) return [];
|
||||||
|
|
||||||
const currentlyLoadedIndex = files.findIndex((project) => project.filename === lastLoadedProject);
|
const currentlyLoadedIndex = files.findIndex((project) => project.filename === lastLoadedProject);
|
||||||
const projectFiles = [...files];
|
const projectFiles = [...files];
|
||||||
const current = projectFiles.splice(currentlyLoadedIndex, 1)?.[0];
|
const current = projectFiles.splice(currentlyLoadedIndex, 1)?.[0];
|
||||||
|
|
||||||
return [current, ...projectFiles];
|
return [current, ...projectFiles];
|
||||||
}, [data.files?.length, files, lastLoadedProject]);
|
}, [data.files.length, files, lastLoadedProject]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Table>
|
<Panel.Table>
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ function ActionMenu({
|
|||||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||||
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
||||||
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
||||||
<MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
|
{current && <MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>}
|
||||||
<MenuItem isDisabled={current} onClick={handleDelete}>
|
<MenuItem isDisabled={current} onClick={handleDelete}>
|
||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import ManageProjects from './ManageProjects';
|
|||||||
import ProjectData from './ProjectData';
|
import ProjectData from './ProjectData';
|
||||||
|
|
||||||
export default function ProjectPanel({ location }: PanelBaseProps) {
|
export default function ProjectPanel({ location }: PanelBaseProps) {
|
||||||
const projectRef = useScrollIntoView<HTMLDivElement>('data', location);
|
const projectRef = useScrollIntoView<HTMLDivElement>('project', location);
|
||||||
const manageRef = useScrollIntoView<HTMLDivElement>('manage', location);
|
const manageRef = useScrollIntoView<HTMLDivElement>('manage', location);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+2
-2
@@ -4,11 +4,11 @@ import { IoPencil } from '@react-icons/all-files/io5/IoPencil';
|
|||||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||||
import { CustomField, CustomFieldLabel } from 'ontime-types';
|
import { CustomField, CustomFieldLabel } from 'ontime-types';
|
||||||
|
|
||||||
import Swatch from '../../../../../common/components/input/colour-input/Swatch';
|
import Swatch from '../../../../common/components/input/colour-input/Swatch';
|
||||||
|
|
||||||
import CustomFieldForm from './CustomFieldForm';
|
import CustomFieldForm from './CustomFieldForm';
|
||||||
|
|
||||||
import style from '../FeatureSettings.module.scss';
|
import style from './ProjectSettingsPanel.module.scss';
|
||||||
|
|
||||||
interface CustomFieldEntryProps {
|
interface CustomFieldEntryProps {
|
||||||
field: string;
|
field: string;
|
||||||
+4
-4
@@ -4,11 +4,11 @@ import { Button, Input } from '@chakra-ui/react';
|
|||||||
import { CustomField } from 'ontime-types';
|
import { CustomField } from 'ontime-types';
|
||||||
import { isAlphanumeric } from 'ontime-utils';
|
import { isAlphanumeric } from 'ontime-utils';
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import SwatchSelect from '../../../../../common/components/input/colour-input/SwatchSelect';
|
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
|
||||||
import * as Panel from '../../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import style from '../FeatureSettings.module.scss';
|
import style from './ProjectSettingsPanel.module.scss';
|
||||||
|
|
||||||
interface CustomFieldsFormProps {
|
interface CustomFieldsFormProps {
|
||||||
onSubmit: (field: CustomField) => Promise<void>;
|
onSubmit: (field: CustomField) => Promise<void>;
|
||||||
+1
-18
@@ -4,12 +4,7 @@
|
|||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5px;
|
gap: 0.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
.actionButtons {
|
|
||||||
display: flex;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fieldForm {
|
.fieldForm {
|
||||||
@@ -25,15 +20,3 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fit {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aliasConstrain {
|
|
||||||
min-width: 12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Alert, AlertDescription, AlertIcon, Button } from '@chakra-ui/react';
|
||||||
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
|
import { CustomField, CustomFieldLabel } from 'ontime-types';
|
||||||
|
|
||||||
|
import { deleteCustomField, editCustomField, postCustomField } from '../../../../common/api/customFields';
|
||||||
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
|
import useCustomFields from '../../../../common/hooks-query/useCustomFields';
|
||||||
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
import CustomFieldEntry from './CustomFieldEntry';
|
||||||
|
import CustomFieldForm from './CustomFieldForm';
|
||||||
|
|
||||||
|
const customFieldsDocsUrl = 'https://docs.getontime.no/features/custom-fields/';
|
||||||
|
|
||||||
|
export default function ProjectSettingsPanel() {
|
||||||
|
const { data, refetch } = useCustomFields();
|
||||||
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
|
||||||
|
const handleInitiateCreate = () => {
|
||||||
|
setIsAdding(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsAdding(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async (customField: CustomField) => {
|
||||||
|
await postCustomField(customField);
|
||||||
|
refetch();
|
||||||
|
setIsAdding(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditField = async (label: CustomFieldLabel, customField: CustomField) => {
|
||||||
|
await editCustomField(label, customField);
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (label: string) => {
|
||||||
|
try {
|
||||||
|
await deleteCustomField(label);
|
||||||
|
refetch();
|
||||||
|
} catch (_error) {
|
||||||
|
/** we do not handle errors here */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Panel.Header>Project Settings</Panel.Header>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Card>
|
||||||
|
<Panel.SubHeader>
|
||||||
|
Custom fields
|
||||||
|
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleInitiateCreate}>
|
||||||
|
New
|
||||||
|
</Button>
|
||||||
|
</Panel.SubHeader>
|
||||||
|
<Panel.Divider />
|
||||||
|
<Panel.Section>
|
||||||
|
<Alert status='info' variant='ontime-on-dark-info'>
|
||||||
|
<AlertIcon />
|
||||||
|
<AlertDescription>
|
||||||
|
Custom fields allow for additional information to be added to an event (eg. light, sound, camera).{' '}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
This data is not used by Ontime.
|
||||||
|
<ExternalLink href={customFieldsDocsUrl}>See the docs</ExternalLink>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</Panel.Section>
|
||||||
|
<Panel.Section>
|
||||||
|
{isAdding && <CustomFieldForm onSubmit={handleCreate} onCancel={handleCancel} />}
|
||||||
|
<Panel.Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Colour</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(data).map(([key, { colour, label }]) => {
|
||||||
|
return (
|
||||||
|
<CustomFieldEntry
|
||||||
|
key={key}
|
||||||
|
field={key}
|
||||||
|
colour={colour}
|
||||||
|
label={label}
|
||||||
|
onEdit={handleEditField}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Panel.Table>
|
||||||
|
</Panel.Section>
|
||||||
|
</Panel.Card>
|
||||||
|
</Panel.Section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,26 +1,13 @@
|
|||||||
import { useRef } from 'react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogBody,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
Button,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import useElectronEvent from '../../../../common/hooks/useElectronEvent';
|
import useElectronEvent from '../../../../common/hooks/useElectronEvent';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
export default function ShutdownPanel() {
|
export default function ShutdownPanel() {
|
||||||
const { isElectron, sendToElectron } = useElectronEvent();
|
const { isElectron, sendToElectron } = useElectronEvent();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
|
||||||
|
|
||||||
const sendShutdown = () => {
|
const sendShutdown = () => {
|
||||||
sendToElectron('shutdown', 'now');
|
sendToElectron('shutdown', 'now');
|
||||||
onClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -31,29 +18,9 @@ export default function ShutdownPanel() {
|
|||||||
This will shutdown the Ontime server. <br />
|
This will shutdown the Ontime server. <br />
|
||||||
The runtime state will be lost, but your project is kept for next time.
|
The runtime state will be lost, but your project is kept for next time.
|
||||||
</Panel.Paragraph>
|
</Panel.Paragraph>
|
||||||
<Button colorScheme='red' onClick={onOpen} maxWidth='350px' isDisabled={!isElectron}>
|
<Button colorScheme='red' onClick={sendShutdown} isDisabled={!isElectron}>
|
||||||
Shutdown ontime
|
Shutdown ontime
|
||||||
</Button>
|
</Button>
|
||||||
<AlertDialog variant='ontime' isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
|
||||||
<AlertDialogOverlay>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
|
||||||
Ontime Shutdown
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogBody>
|
|
||||||
This will shutdown the Ontime server. <br /> Are you sure?
|
|
||||||
</AlertDialogBody>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<Button ref={cancelRef} onClick={onClose} variant='ontime-ghosted-white'>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button colorScheme='red' onClick={sendShutdown} ml={4}>
|
|
||||||
Shutdown
|
|
||||||
</Button>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialogOverlay>
|
|
||||||
</AlertDialog>
|
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export default function GSheetSetup(props: GSheetSetupProps) {
|
|||||||
const patchStepData = useSheetStore((state) => state.patchStepData);
|
const patchStepData = useSheetStore((state) => state.patchStepData);
|
||||||
const authenticationStatus = useSheetStore((state) => state.authenticationStatus);
|
const authenticationStatus = useSheetStore((state) => state.authenticationStatus);
|
||||||
const setAuthenticationStatus = useSheetStore((state) => state.setAuthenticationStatus);
|
const setAuthenticationStatus = useSheetStore((state) => state.setAuthenticationStatus);
|
||||||
const authenticationError = useSheetStore((state) => state.stepData.authenticate.error);
|
|
||||||
|
|
||||||
/** Check if we are authenticated */
|
/** Check if we are authenticated */
|
||||||
const getAuthStatus = async () => {
|
const getAuthStatus = async () => {
|
||||||
@@ -45,7 +44,6 @@ export default function GSheetSetup(props: GSheetSetupProps) {
|
|||||||
|
|
||||||
/** check if the current session has been authenticated */
|
/** check if the current session has been authenticated */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
patchStepData({ authenticate: { available: false, error: '' } });
|
|
||||||
untilAuthenticated();
|
untilAuthenticated();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -152,7 +150,7 @@ export default function GSheetSetup(props: GSheetSetupProps) {
|
|||||||
</Panel.Title>
|
</Panel.Title>
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.Description>Upload Client Secret provided by Google</Panel.Description>
|
<Panel.Description>Upload Client Secret provided by Google</Panel.Description>
|
||||||
<Panel.Error>{authenticationError}</Panel.Error>
|
<Panel.Error>{undefined}</Panel.Error>
|
||||||
<Input
|
<Input
|
||||||
type='file'
|
type='file'
|
||||||
onChange={handleClientSecret}
|
onChange={handleClientSecret}
|
||||||
|
|||||||
+2
-3
@@ -1,13 +1,12 @@
|
|||||||
import { ImportCustom } from 'ontime-utils';
|
import { ImportCustom } from 'ontime-utils';
|
||||||
|
|
||||||
import { convertToImportMap, NamedImportMap } from '../importMapUtils';
|
import { convertToImportMap } from '../importMapUtils';
|
||||||
|
|
||||||
describe('convertToImportMap', () => {
|
describe('convertToImportMap', () => {
|
||||||
it('converts a namedImportMap to a importMap', () => {
|
it('converts a namedImportMap to a importMap', () => {
|
||||||
const defaultNamedImporMap = {
|
const defaultNamedImporMap = {
|
||||||
Worksheet: 'event schedule',
|
Worksheet: 'event schedule',
|
||||||
Start: 'time start',
|
Start: 'time start',
|
||||||
'Link start': 'link start',
|
|
||||||
End: 'time end',
|
End: 'time end',
|
||||||
Duration: 'duration',
|
Duration: 'duration',
|
||||||
Cue: 'cue',
|
Cue: 'cue',
|
||||||
@@ -27,7 +26,7 @@ describe('convertToImportMap', () => {
|
|||||||
{ ontimeName: 'EmptyImportName', importName: '' },
|
{ ontimeName: 'EmptyImportName', importName: '' },
|
||||||
{ ontimeName: '', importName: 'EmptyOntimeName' },
|
{ ontimeName: '', importName: 'EmptyOntimeName' },
|
||||||
] as ImportCustom[],
|
] as ImportCustom[],
|
||||||
} as NamedImportMap;
|
};
|
||||||
|
|
||||||
const importMap = convertToImportMap(defaultNamedImporMap);
|
const importMap = convertToImportMap(defaultNamedImporMap);
|
||||||
expect(importMap.custom).toStrictEqual({
|
expect(importMap.custom).toStrictEqual({
|
||||||
|
|||||||
+2
-25
@@ -6,7 +6,6 @@ export type NamedImportMap = typeof namedImportMap;
|
|||||||
export const namedImportMap = {
|
export const namedImportMap = {
|
||||||
Worksheet: 'event schedule',
|
Worksheet: 'event schedule',
|
||||||
Start: 'time start',
|
Start: 'time start',
|
||||||
'Link start': 'link start',
|
|
||||||
End: 'time end',
|
End: 'time end',
|
||||||
Duration: 'duration',
|
Duration: 'duration',
|
||||||
Cue: 'cue',
|
Cue: 'cue',
|
||||||
@@ -22,15 +21,6 @@ export const namedImportMap = {
|
|||||||
custom: [] as ImportCustom[],
|
custom: [] as ImportCustom[],
|
||||||
};
|
};
|
||||||
|
|
||||||
function isNamedImportMap(obj: unknown): obj is NamedImportMap {
|
|
||||||
if (typeof obj !== 'object' || obj === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(namedImportMap);
|
|
||||||
return keys.every((key) => Object.hasOwn(obj, key));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
|
export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
|
||||||
const custom = namedImportMap.custom.reduce((accumulator, { ontimeName, importName }) => {
|
const custom = namedImportMap.custom.reduce((accumulator, { ontimeName, importName }) => {
|
||||||
if (ontimeName && importName) {
|
if (ontimeName && importName) {
|
||||||
@@ -42,7 +32,6 @@ export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
|
|||||||
return {
|
return {
|
||||||
worksheet: namedImportMap.Worksheet,
|
worksheet: namedImportMap.Worksheet,
|
||||||
timeStart: namedImportMap.Start,
|
timeStart: namedImportMap.Start,
|
||||||
linkStart: namedImportMap['Link start'],
|
|
||||||
timeEnd: namedImportMap.End,
|
timeEnd: namedImportMap.End,
|
||||||
duration: namedImportMap.Duration,
|
duration: namedImportMap.Duration,
|
||||||
cue: namedImportMap.Cue,
|
cue: namedImportMap.Cue,
|
||||||
@@ -63,22 +52,10 @@ export function persistImportMap(options: NamedImportMap) {
|
|||||||
localStorage.setItem('ontime-import-options', JSON.stringify(options));
|
localStorage.setItem('ontime-import-options', JSON.stringify(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPersistImportMap(): unknown {
|
export function getPersistedOptions(): NamedImportMap {
|
||||||
const options = localStorage.getItem('ontime-import-options');
|
const options = localStorage.getItem('ontime-import-options');
|
||||||
if (!options) {
|
if (!options) {
|
||||||
throw new Error('no import options found');
|
return namedImportMap;
|
||||||
}
|
}
|
||||||
return JSON.parse(options);
|
return JSON.parse(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPersistedOptions(): NamedImportMap {
|
|
||||||
try {
|
|
||||||
const options = getPersistImportMap();
|
|
||||||
if (!isNamedImportMap(options)) {
|
|
||||||
return namedImportMap;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
} catch {
|
|
||||||
return namedImportMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+10
-25
@@ -1,27 +1,12 @@
|
|||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nowrap {
|
.nowrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr .secondaryRow {
|
tr .secondaryRow {
|
||||||
background-color: $white-7;
|
background-color: $white-7;
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkStartActive {
|
|
||||||
color: $active-indicator;
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subdued {
|
|
||||||
opacity: $opacity-disabled;
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-6
@@ -1,5 +1,4 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { IoLink } from '@react-icons/all-files/io5/IoLink';
|
|
||||||
import { CustomFields, isOntimeBlock, isOntimeEvent, OntimeRundown } from 'ontime-types';
|
import { CustomFields, isOntimeBlock, isOntimeEvent, OntimeRundown } from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -84,10 +83,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
|||||||
</td>
|
</td>
|
||||||
<td className={style.nowrap}>{event.cue}</td>
|
<td className={style.nowrap}>{event.cue}</td>
|
||||||
<td>{event.title}</td>
|
<td>{event.title}</td>
|
||||||
<td className={style.flex}>
|
<td>{millisToString(event.timeStart)}</td>
|
||||||
<span className={event.linkStart ? style.subdued : undefined}>{millisToString(event.timeStart)}</span>
|
|
||||||
{event.linkStart && <IoLink className={style.linkStartActive} />}
|
|
||||||
</td>
|
|
||||||
<td>{millisToString(event.timeEnd)}</td>
|
<td>{millisToString(event.timeEnd)}</td>
|
||||||
<td>{millisToString(event.duration)}</td>
|
<td>{millisToString(event.duration)}</td>
|
||||||
<td>{millisToString(event.timeWarning)}</td>
|
<td>{millisToString(event.timeWarning)}</td>
|
||||||
@@ -105,7 +101,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
|||||||
fieldHeaders.map((field) => {
|
fieldHeaders.map((field) => {
|
||||||
let value = '';
|
let value = '';
|
||||||
if (field in event.custom) {
|
if (field in event.custom) {
|
||||||
value = event.custom[field];
|
value = event.custom[field].value;
|
||||||
}
|
}
|
||||||
return <td key={field}>{value}</td>;
|
return <td key={field}>{value}</td>;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ export default function useGoogleSheet() {
|
|||||||
/** whether the current session has been authenticated */
|
/** whether the current session has been authenticated */
|
||||||
const verifyAuth = async (): Promise<{ authenticated: AuthenticationStatus; sheetId: string } | void> => {
|
const verifyAuth = async (): Promise<{ authenticated: AuthenticationStatus; sheetId: string } | void> => {
|
||||||
try {
|
try {
|
||||||
return await verifyAuthenticationStatus();
|
return verifyAuthenticationStatus();
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
patchStepData({ authenticate: { available: false, error: maybeAxiosError(error) } });
|
/** we do not handle errors here */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,18 +37,18 @@ export default function useGoogleSheet() {
|
|||||||
sheetId: string,
|
sheetId: string,
|
||||||
): Promise<{ verification_url: string; user_code: string } | void> => {
|
): Promise<{ verification_url: string; user_code: string } | void> => {
|
||||||
try {
|
try {
|
||||||
return await requestConnection(file, sheetId);
|
return requestConnection(file, sheetId);
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
patchStepData({ authenticate: { available: false, error: maybeAxiosError(error) } });
|
/** we do not handle errors here */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** requests the revoking of an existing authenticated session */
|
/** requests the revoking of an existing authenticated session */
|
||||||
const revoke = async (): Promise<{ authenticated: AuthenticationStatus } | void> => {
|
const revoke = async (): Promise<{ authenticated: AuthenticationStatus } | void> => {
|
||||||
try {
|
try {
|
||||||
return await revokeAuthentication();
|
return revokeAuthentication();
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
patchStepData({ authenticate: { available: false, error: maybeAxiosError(error) } });
|
/** we do not handle errors here */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,28 +11,23 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
|||||||
{
|
{
|
||||||
id: 'project',
|
id: 'project',
|
||||||
label: 'Project',
|
label: 'Project',
|
||||||
secondary: [
|
secondary: [{ id: 'project__manage', label: 'Manage project files' }],
|
||||||
{ id: 'project__data', label: 'Project data' },
|
|
||||||
{ id: 'project__manage', label: 'Manage projects' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'general',
|
id: 'general',
|
||||||
label: 'App Settings',
|
label: 'App Settings',
|
||||||
secondary: [
|
secondary: [
|
||||||
{ id: 'general__settings', label: 'General settings' },
|
{ id: 'general__manage', label: 'Manage Ontime settings' },
|
||||||
{ id: 'general__editor', label: 'Editor settings' },
|
|
||||||
{ id: 'general__view', label: 'View settings' },
|
{ id: 'general__view', label: 'View settings' },
|
||||||
|
{ id: 'general__urlpresets', label: 'URL presets' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'feature_settings',
|
id: 'project_settings',
|
||||||
label: 'Feature Settings',
|
label: 'Project Settings',
|
||||||
secondary: [
|
secondary: [{ id: 'project_settings__custom', label: 'Custom fields' }],
|
||||||
{ id: 'feature_settings__custom', label: 'Custom fields' },
|
|
||||||
{ id: 'feature_settings__urlpresets', label: 'URL Presets' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
{ id: 'interface', label: 'Interface', secondary: [{ id: 'interface__editor', label: 'Editor settings' }] },
|
||||||
{
|
{
|
||||||
id: 'sources',
|
id: 'sources',
|
||||||
label: 'Data Sources',
|
label: 'Data Sources',
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import "./MessageControl.module.scss";
|
|
||||||
|
|
||||||
.inputItems {
|
.inputItems {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
@@ -8,5 +6,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
@extend .label;
|
font-size: $inner-section-text-size;
|
||||||
|
color: $label-gray;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $action-text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,3 @@
|
|||||||
gap: $element-spacing;
|
gap: $element-spacing;
|
||||||
margin-top: -0.5rem;
|
margin-top: -0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.singleAction {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $element-spacing;
|
|
||||||
margin-top: $element-inner-spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: $inner-section-text-size;
|
|
||||||
color: $label-gray;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: $action-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,14 +17,29 @@ export default function MessageControl() {
|
|||||||
const message = useMessageControl();
|
const message = useMessageControl();
|
||||||
const blink = message.timer.blink;
|
const blink = message.timer.blink;
|
||||||
const blackout = message.timer.blackout;
|
const blackout = message.timer.blackout;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.messageContainer}>
|
<div className={style.messageContainer}>
|
||||||
|
<InputRow
|
||||||
|
label='Public / Backstage screen message'
|
||||||
|
placeholder='Shown in public and backstage screens'
|
||||||
|
text={message.public.text || ''}
|
||||||
|
visible={message.public.visible || false}
|
||||||
|
changeHandler={(newValue) => setMessage.publicText(newValue)}
|
||||||
|
actionHandler={() => setMessage.publicVisible(!message.public.visible)}
|
||||||
|
/>
|
||||||
|
<InputRow
|
||||||
|
label='Lower third message'
|
||||||
|
placeholder='Shown in lower third'
|
||||||
|
text={message.lower.text || ''}
|
||||||
|
visible={message.lower.visible || false}
|
||||||
|
changeHandler={(newValue) => setMessage.lowerText(newValue)}
|
||||||
|
actionHandler={() => setMessage.lowerVisible(!message.lower.visible)}
|
||||||
|
/>
|
||||||
<InputRow
|
<InputRow
|
||||||
label='Timer'
|
label='Timer'
|
||||||
placeholder='Message shown in stage timer'
|
placeholder='Message shown in stage timer'
|
||||||
text={message.timer.text}
|
text={message.timer.text || ''}
|
||||||
visible={message.timer.visible}
|
visible={message.timer.visible || false}
|
||||||
changeHandler={(newValue) => setMessage.timerText(newValue)}
|
changeHandler={(newValue) => setMessage.timerText(newValue)}
|
||||||
actionHandler={() => setMessage.timerVisible(!message.timer.visible)}
|
actionHandler={() => setMessage.timerVisible(!message.timer.visible)}
|
||||||
/>
|
/>
|
||||||
@@ -54,8 +69,8 @@ export default function MessageControl() {
|
|||||||
label='External Message (read only)'
|
label='External Message (read only)'
|
||||||
placeholder={enDash}
|
placeholder={enDash}
|
||||||
readonly
|
readonly
|
||||||
text={message.external.text}
|
text={message.external.text || ''}
|
||||||
visible={message.external.visible}
|
visible={message.external.visible || false}
|
||||||
changeHandler={noop}
|
changeHandler={noop}
|
||||||
actionHandler={noop}
|
actionHandler={noop}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { Playback } from 'ontime-types';
|
|||||||
|
|
||||||
import { usePlaybackControl } from '../../../common/hooks/useSocket';
|
import { usePlaybackControl } from '../../../common/hooks/useSocket';
|
||||||
|
|
||||||
import AddTime from './add-time/AddTime';
|
import { ExtraTimer } from './extra-timer/ExtraTimer';
|
||||||
import { AuxTimer } from './aux-timer/AuxTimer';
|
|
||||||
import PlaybackButtons from './playback-buttons/PlaybackButtons';
|
import PlaybackButtons from './playback-buttons/PlaybackButtons';
|
||||||
import PlaybackTimer from './playback-timer/PlaybackTimer';
|
import PlaybackTimer from './playback-timer/PlaybackTimer';
|
||||||
|
|
||||||
@@ -14,15 +13,13 @@ export default function PlaybackControl() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.mainContainer}>
|
<div className={style.mainContainer}>
|
||||||
<PlaybackTimer playback={data.playback as Playback}>
|
<PlaybackTimer playback={data.playback as Playback} />
|
||||||
<AddTime playback={data.playback} />
|
|
||||||
</PlaybackTimer>
|
|
||||||
<PlaybackButtons
|
<PlaybackButtons
|
||||||
playback={data.playback}
|
playback={data.playback}
|
||||||
numEvents={data.numEvents}
|
numEvents={data.numEvents}
|
||||||
selectedEventIndex={data.selectedEventIndex}
|
selectedEventIndex={data.selectedEventIndex}
|
||||||
/>
|
/>
|
||||||
<AuxTimer />
|
<ExtraTimer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
.addTime {
|
|
||||||
grid-area: addtime;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $element-inner-spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addButtons {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: $element-inner-spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tallButtons {
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { Tooltip } from '@chakra-ui/react';
|
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
|
||||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
|
||||||
import { Playback } from 'ontime-types';
|
|
||||||
import { MILLIS_PER_HOUR, MILLIS_PER_SECOND } from 'ontime-utils';
|
|
||||||
|
|
||||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
|
||||||
import { useLocalStorage } from '../../../../common/hooks/useLocalStorage';
|
|
||||||
import { setPlayback } from '../../../../common/hooks/useSocket';
|
|
||||||
import { forgivingStringToMillis } from '../../../../common/utils/dateConfig';
|
|
||||||
import { tooltipDelayMid } from '../../../../ontimeConfig';
|
|
||||||
import TapButton from '../tap-button/TapButton';
|
|
||||||
|
|
||||||
import style from './AddTime.module.scss';
|
|
||||||
|
|
||||||
interface AddTimeProps {
|
|
||||||
playback: Playback;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AddTime(props: AddTimeProps) {
|
|
||||||
const { playback } = props;
|
|
||||||
const [time, setTime] = useLocalStorage('add-time', 300_000); // 5 minutes
|
|
||||||
|
|
||||||
const handleTimeChange = (_field: string, value: string) => {
|
|
||||||
const newTime = forgivingStringToMillis(value);
|
|
||||||
// cap add time to 1 hour
|
|
||||||
setTime(Math.min(newTime, MILLIS_PER_HOUR));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddTime = (direction: 'add' | 'remove') => {
|
|
||||||
// API expects input in seconds
|
|
||||||
if (direction === 'add') {
|
|
||||||
setPlayback.addTime(time / MILLIS_PER_SECOND);
|
|
||||||
} else {
|
|
||||||
setPlayback.addTime((-1 * time) / MILLIS_PER_SECOND);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canAddTime = playback === Playback.Play || playback === Playback.Pause;
|
|
||||||
const doDisableButtons = !canAddTime || time === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.addTime}>
|
|
||||||
<TimeInput name='addtime' submitHandler={handleTimeChange} time={time} placeholder='Add time' />
|
|
||||||
<div className={style.addButtons}>
|
|
||||||
<Tooltip label='Remove time' openDelay={tooltipDelayMid} shouldWrapChildren>
|
|
||||||
<TapButton onClick={() => handleAddTime('remove')} disabled={doDisableButtons} className={style.tallButtons}>
|
|
||||||
<IoRemove />
|
|
||||||
</TapButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label='Add time' openDelay={tooltipDelayMid} shouldWrapChildren>
|
|
||||||
<TapButton onClick={() => handleAddTime('add')} disabled={doDisableButtons} className={style.tallButtons}>
|
|
||||||
<IoAdd />
|
|
||||||
</TapButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+16
-16
@@ -6,16 +6,16 @@ import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
|||||||
import { Playback, SimpleDirection, SimplePlayback } from 'ontime-types';
|
import { Playback, SimpleDirection, SimplePlayback } from 'ontime-types';
|
||||||
|
|
||||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||||
import { setAuxTimer, useAuxTimerControl, useAuxTimerTime } from '../../../../common/hooks/useSocket';
|
import { setExtraTimer, useExtraTimerControl, useExtraTimerTime } from '../../../../common/hooks/useSocket';
|
||||||
import { forgivingStringToMillis } from '../../../../common/utils/dateConfig';
|
import { forgivingStringToMillis } from '../../../../common/utils/dateConfig';
|
||||||
import TapButton from '../tap-button/TapButton';
|
import TapButton from '../tap-button/TapButton';
|
||||||
|
|
||||||
import style from './AuxTimer.module.scss';
|
import style from './ExtraTimer.module.scss';
|
||||||
|
|
||||||
export function AuxTimer() {
|
export function ExtraTimer() {
|
||||||
const { playback, direction } = useAuxTimerControl();
|
const { playback, direction } = useExtraTimerControl();
|
||||||
|
|
||||||
const { start, pause, stop, setDirection } = setAuxTimer;
|
const { start, pause, stop, setDirection } = setExtraTimer;
|
||||||
|
|
||||||
const toggleDirection = () => {
|
const toggleDirection = () => {
|
||||||
const newDirection = direction === SimpleDirection.CountDown ? SimpleDirection.CountUp : SimpleDirection.CountDown;
|
const newDirection = direction === SimpleDirection.CountDown ? SimpleDirection.CountUp : SimpleDirection.CountDown;
|
||||||
@@ -30,10 +30,10 @@ export function AuxTimer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.extraRow}>
|
<div className={style.extraRow}>
|
||||||
<AuxTimerInput />
|
<ExtraTimeInput />
|
||||||
<TapButton onClick={toggleDirection} aspect='tight'>
|
<TapButton onClick={toggleDirection} aspect='tight'>
|
||||||
{direction === SimpleDirection.CountDown && <IoArrowDown data-testid='aux-timer-direction' />}
|
{direction === SimpleDirection.CountDown && <IoArrowDown />}
|
||||||
{direction === SimpleDirection.CountUp && <IoArrowUp data-testid='aux-timer-direction' />}
|
{direction === SimpleDirection.CountUp && <IoArrowUp />}
|
||||||
</TapButton>
|
</TapButton>
|
||||||
|
|
||||||
<TapButton
|
<TapButton
|
||||||
@@ -42,7 +42,7 @@ export function AuxTimer() {
|
|||||||
active={playback === SimplePlayback.Start}
|
active={playback === SimplePlayback.Start}
|
||||||
disabled={!userCan.start}
|
disabled={!userCan.start}
|
||||||
>
|
>
|
||||||
<IoPlay data-testid='aux-timer-start' />
|
<IoPlay />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
<TapButton
|
<TapButton
|
||||||
onClick={pause}
|
onClick={pause}
|
||||||
@@ -50,25 +50,25 @@ export function AuxTimer() {
|
|||||||
active={playback === SimplePlayback.Pause}
|
active={playback === SimplePlayback.Pause}
|
||||||
disabled={!userCan.pause}
|
disabled={!userCan.pause}
|
||||||
>
|
>
|
||||||
<IoPause data-testid='aux-timer-pause' />
|
<IoPause />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
<TapButton onClick={stop} theme={Playback.Stop} disabled={!userCan.stop}>
|
<TapButton onClick={stop} theme={Playback.Stop} disabled={!userCan.stop}>
|
||||||
<IoStop data-testid='aux-timer-stop' />
|
<IoStop />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuxTimerInput() {
|
function ExtraTimeInput() {
|
||||||
const time = useAuxTimerTime();
|
const time = useExtraTimerTime();
|
||||||
const { setDuration } = setAuxTimer;
|
const { setTime } = setExtraTimer;
|
||||||
|
|
||||||
const handleTimeUpdate = (_field: string, value: string) => {
|
const handleTimeUpdate = (_field: string, value: string) => {
|
||||||
const newTime = forgivingStringToMillis(value);
|
const newTime = forgivingStringToMillis(value);
|
||||||
setDuration(newTime / 1000); //frontend api is seconds based;
|
setTime(newTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeInput<'auxTimer'> submitHandler={handleTimeUpdate} name='auxTimer' time={time} placeholder='Aux Timer 1' />
|
<TimeInput<'extraTimer'> submitHandler={handleTimeUpdate} name='extraTimer' time={time} placeholder='Timer 1' />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,6 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
|||||||
const noEvents = numEvents === 0;
|
const noEvents = numEvents === 0;
|
||||||
|
|
||||||
const disableGo = isRolling || noEvents || (isLast && !isArmed);
|
const disableGo = isRolling || noEvents || (isLast && !isArmed);
|
||||||
const disableNext = isRolling || noEvents || isLast;
|
|
||||||
const disablePrev = isRolling || noEvents || isFirst;
|
const disablePrev = isRolling || noEvents || isFirst;
|
||||||
|
|
||||||
const playbackCan = validatePlayback(playback);
|
const playbackCan = validatePlayback(playback);
|
||||||
@@ -74,7 +73,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
|||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label='Next event' openDelay={tooltipDelayMid}>
|
<Tooltip label='Next event' openDelay={tooltipDelayMid}>
|
||||||
<TapButton onClick={setPlayback.next} disabled={disableNext}>
|
<TapButton onClick={setPlayback.next} disabled={disableGo}>
|
||||||
<IoPlaySkipForward />
|
<IoPlaySkipForward />
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
+55
-30
@@ -1,17 +1,16 @@
|
|||||||
.timeContainer {
|
.timeContainer {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'indicators timer addtime'
|
'ind clk clk btn'
|
||||||
'status status addtime';
|
'... sta fin btn';
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: 1fr auto;
|
||||||
grid-template-columns: 1.25rem 1fr 6.5rem;
|
grid-template-columns: 1.25rem 1fr 1fr 5rem;
|
||||||
gap: $element-inner-spacing;
|
gap: $element-inner-spacing;
|
||||||
|
justify-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------> INDICATORS
|
|
||||||
|
|
||||||
.indicators {
|
.indicators {
|
||||||
grid-area: indicators;
|
grid-area: ind;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -19,46 +18,72 @@
|
|||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicatorRoll,
|
.indRoll,
|
||||||
.indicatorDelay,
|
.indDelay,
|
||||||
.indicatorNegative {
|
.indNegative {
|
||||||
background-color: $black-10;
|
background-color: $black-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicatorRoll,
|
.indRoll,
|
||||||
.indicatorDelay {
|
.indRollActive,
|
||||||
|
.indDelay,
|
||||||
|
.indDelayActive {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
width: 10px;
|
width: 12px;
|
||||||
height: 10px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicatorRoll[data-active='true'] {
|
.indRollActive {
|
||||||
background-color: $ontime-roll;
|
background-color: $ontime-roll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicatorDelay[data-active='true'] {
|
.indNegative,
|
||||||
background-color: $ontime-delay;
|
.indNegativeActive {
|
||||||
}
|
|
||||||
|
|
||||||
.indicatorNegative {
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
|
|
||||||
&[data-active='true'] {
|
|
||||||
background-color: $playback-negative;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------> LABELS
|
.indNegativeActive {
|
||||||
|
background-color: $playback-negative;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.indDelayActive {
|
||||||
grid-area: status;
|
background-color: $ontime-delay;
|
||||||
height: 1.5rem;
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
grid-area: btn;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
width: 100%;
|
||||||
|
gap: $element-inner-spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minus {
|
||||||
|
grid-area: min;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $section-spacing;
|
flex-direction: column;
|
||||||
margin-left: 1.5rem;
|
}
|
||||||
|
|
||||||
|
.start,
|
||||||
|
.finish,
|
||||||
|
.roll {
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start {
|
||||||
|
grid-area: sta;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finish {
|
||||||
|
grid-area: fin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roll {
|
||||||
|
grid-area: 2 / 2 / 2 / 4 ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { Playback } from 'ontime-types';
|
import { Playback } from 'ontime-types';
|
||||||
import { dayInMs, millisToMinutes, millisToSeconds, millisToString } from 'ontime-utils';
|
import { dayInMs, millisToMinutes, millisToSeconds, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import { useTimer } from '../../../../common/hooks/useSocket';
|
import { setPlayback, useTimer } from '../../../../common/hooks/useSocket';
|
||||||
|
import { tooltipDelayMid } from '../../../../ontimeConfig';
|
||||||
|
import TapButton from '../tap-button/TapButton';
|
||||||
import TimerDisplay from '../timer-display/TimerDisplay';
|
import TimerDisplay from '../timer-display/TimerDisplay';
|
||||||
|
|
||||||
import style from './PlaybackTimer.module.scss';
|
import style from './PlaybackTimer.module.scss';
|
||||||
@@ -12,30 +13,8 @@ interface PlaybackTimerProps {
|
|||||||
playback: Playback;
|
playback: Playback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAddedTimeLabel(addedTime: number) {
|
export default function PlaybackTimer(props: PlaybackTimerProps) {
|
||||||
function resolveClosestUnit(ms: number) {
|
const { playback } = props;
|
||||||
if (ms < 6000) {
|
|
||||||
return `${millisToSeconds(ms)} seconds`;
|
|
||||||
} else if (ms < 12000) {
|
|
||||||
return '1 minute';
|
|
||||||
} else {
|
|
||||||
return `${millisToMinutes(ms)} minutes`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addedTime > 0) {
|
|
||||||
return `Added ${resolveClosestUnit(addedTime)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addedTime < 0) {
|
|
||||||
return `Removed ${resolveClosestUnit(addedTime)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PlaybackTimer(props: PropsWithChildren<PlaybackTimerProps>) {
|
|
||||||
const { playback, children } = props;
|
|
||||||
const timer = useTimer();
|
const timer = useTimer();
|
||||||
|
|
||||||
const started = millisToString(timer.startedAt);
|
const started = millisToString(timer.startedAt);
|
||||||
@@ -43,43 +22,88 @@ export default function PlaybackTimer(props: PropsWithChildren<PlaybackTimerProp
|
|||||||
const finish = millisToString(expectedFinish);
|
const finish = millisToString(expectedFinish);
|
||||||
|
|
||||||
const isRolling = playback === Playback.Roll;
|
const isRolling = playback === Playback.Roll;
|
||||||
|
const isStopped = playback === Playback.Stop;
|
||||||
const isWaiting = timer.secondaryTimer !== null && timer.secondaryTimer > 0 && timer.current === null;
|
const isWaiting = timer.secondaryTimer !== null && timer.secondaryTimer > 0 && timer.current === null;
|
||||||
|
const disableButtons = isStopped || isRolling;
|
||||||
const isOvertime = timer.current !== null && timer.current < 0;
|
const isOvertime = timer.current !== null && timer.current < 0;
|
||||||
const hasAddedTime = Boolean(timer.addedTime);
|
const hasAddedTime = Boolean(timer.addedTime);
|
||||||
|
|
||||||
const rollLabel = isRolling ? 'Roll mode active' : '';
|
const rollLabel = isRolling ? 'Roll mode active' : '';
|
||||||
|
|
||||||
const addedTimeLabel = resolveAddedTimeLabel(timer.addedTime);
|
const resolveAddedTimeLabel = () => {
|
||||||
|
function resolveClosestUnit(ms: number) {
|
||||||
|
if (ms < 6000) {
|
||||||
|
return `${millisToSeconds(ms)} seconds`;
|
||||||
|
} else if (ms < 12000) {
|
||||||
|
return '1 minute';
|
||||||
|
} else {
|
||||||
|
return `${millisToMinutes(ms)} minutes`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timer.addedTime > 0) {
|
||||||
|
return `Added ${resolveClosestUnit(timer.addedTime)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timer.addedTime < 0) {
|
||||||
|
return `Removed ${resolveClosestUnit(timer.addedTime)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const addedTimeLabel = resolveAddedTimeLabel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.timeContainer}>
|
<div className={style.timeContainer}>
|
||||||
<div className={style.indicators}>
|
<div className={style.indicators}>
|
||||||
<Tooltip label={rollLabel}>
|
<Tooltip label={rollLabel}>
|
||||||
<div className={style.indicatorRoll} data-active={isRolling} />
|
<div className={isRolling ? style.indRollActive : style.indRoll} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className={style.indicatorNegative} data-active={isOvertime} />
|
<div className={isOvertime ? style.indNegativeActive : style.indNegative} />
|
||||||
<Tooltip label={addedTimeLabel}>
|
<Tooltip label={addedTimeLabel}>
|
||||||
<div className={style.indicatorDelay} data-active={hasAddedTime} />
|
<div className={hasAddedTime ? style.indDelayActive : style.indDelay} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<TimerDisplay time={isWaiting ? timer.secondaryTimer : timer.current} />
|
<TimerDisplay time={isWaiting ? timer.secondaryTimer : timer.current} />
|
||||||
<div className={style.status}>
|
{isWaiting ? (
|
||||||
{isWaiting ? (
|
<div className={style.roll}>
|
||||||
<span className={style.rolltag}>Roll: Countdown to start</span>
|
<span className={style.rolltag}>Roll: Countdown to start</span>
|
||||||
) : (
|
</div>
|
||||||
<>
|
) : (
|
||||||
<span className={style.start}>
|
<>
|
||||||
<span className={style.tag}>Started at</span>
|
<div className={style.start}>
|
||||||
<span className={style.time}>{started}</span>
|
<span className={style.tag}>Started at</span>
|
||||||
</span>
|
<span className={style.time}>{started}</span>
|
||||||
<span className={style.finish}>
|
</div>
|
||||||
<span className={style.tag}>Expect end</span>
|
<div className={style.finish}>
|
||||||
<span className={style.time}>{finish}</span>
|
<span className={style.tag}>Expect end</span>
|
||||||
</span>
|
<span className={style.time}>{finish}</span>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
|
<div className={style.btn}>
|
||||||
|
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||||
|
<TapButton onClick={() => setPlayback.addTime(-60)} disabled={disableButtons} aspect='square'>
|
||||||
|
-1
|
||||||
|
</TapButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label='Add 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||||
|
<TapButton onClick={() => setPlayback.addTime(60)} disabled={disableButtons} aspect='square'>
|
||||||
|
+1
|
||||||
|
</TapButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label='Remove 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||||
|
<TapButton onClick={() => setPlayback.addTime(-5 * 60)} disabled={disableButtons} aspect='square'>
|
||||||
|
-5
|
||||||
|
</TapButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label='Add 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||||
|
<TapButton onClick={() => setPlayback.addTime(+5 * 60)} disabled={disableButtons} aspect='square'>
|
||||||
|
+5
|
||||||
|
</TapButton>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@use '../../../../theme/viewerDefs' as *;
|
@use '../../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
grid-area: timer;
|
grid-area: clk;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 18.75rem;
|
max-width: 18.75rem;
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
.tableWrapper {
|
.tableWrapper {
|
||||||
background-color: $ui-black;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 1rem 0.5rem;
|
padding: 1rem 0.5rem;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useCuesheet } from '../../common/hooks/useSocket';
|
|||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||||
import { CuesheetOverview } from '../overview/Overview';
|
import Overview from '../overview/Overview';
|
||||||
|
|
||||||
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
||||||
import { useCuesheetSettings } from './store/CuesheetSettings';
|
import { useCuesheetSettings } from './store/CuesheetSettings';
|
||||||
@@ -53,7 +53,7 @@ export default function CuesheetWrapper() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousValue = event.custom[accessor];
|
const previousValue = event.custom[accessor]?.value;
|
||||||
|
|
||||||
if (previousValue === payload) {
|
if (previousValue === payload) {
|
||||||
return;
|
return;
|
||||||
@@ -85,7 +85,7 @@ export default function CuesheetWrapper() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||||
<CuesheetOverview>
|
<Overview>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Toggle settings'
|
aria-label='Toggle settings'
|
||||||
variant='ontime-subtle-white'
|
variant='ontime-subtle-white'
|
||||||
@@ -100,7 +100,7 @@ export default function CuesheetWrapper() {
|
|||||||
icon={<IoSettingsOutline />}
|
icon={<IoSettingsOutline />}
|
||||||
onClick={() => toggleSettings()}
|
onClick={() => toggleSettings()}
|
||||||
/>
|
/>
|
||||||
</CuesheetOverview>
|
</Overview>
|
||||||
<CuesheetProgress />
|
<CuesheetProgress />
|
||||||
<Cuesheet
|
<Cuesheet
|
||||||
data={flatRundown}
|
data={flatRundown}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ChangeEvent, memo, useCallback, useEffect, useRef, useState } from 'react';
|
import { ChangeEvent, memo, useCallback, useEffect, useState } from 'react';
|
||||||
import { getHotkeyHandler } from '@mantine/hooks';
|
|
||||||
|
|
||||||
import { AutoTextArea } from '../../../common/components/input/auto-text-area/AutoTextArea';
|
import { AutoTextArea } from '../../../common/components/input/auto-text-area/AutoTextArea';
|
||||||
|
|
||||||
@@ -13,24 +12,12 @@ const EditableCell = (props: EditableCellProps) => {
|
|||||||
|
|
||||||
// We need to keep and update the state of the cell normally
|
// We need to keep and update the state of the cell normally
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
const ref = useRef<HTMLAreaElement>();
|
|
||||||
const onChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => setValue(event.target.value), []);
|
const onChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => setValue(event.target.value), []);
|
||||||
|
|
||||||
// We'll only update the external data when the input is blurred
|
// We'll only update the external data when the input is blurred
|
||||||
const onBlur = useCallback(() => handleUpdate(value), [handleUpdate, value]);
|
const onBlur = useCallback(() => handleUpdate(value), [handleUpdate, value]);
|
||||||
|
|
||||||
//TODO: maybe we can unify this with `useReactiveTextInput`
|
|
||||||
const onKeyDown = getHotkeyHandler([
|
|
||||||
['mod + Enter', () => ref.current?.blur()],
|
|
||||||
[
|
|
||||||
'Escape',
|
|
||||||
() => {
|
|
||||||
setValue(initialValue);
|
|
||||||
setTimeout(() => ref.current?.blur());
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// If the initialValue is changed external, sync it up with our state
|
// If the initialValue is changed external, sync it up with our state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(initialValue);
|
setValue(initialValue);
|
||||||
@@ -40,11 +27,9 @@ const EditableCell = (props: EditableCellProps) => {
|
|||||||
<AutoTextArea
|
<AutoTextArea
|
||||||
size='sm'
|
size='sm'
|
||||||
value={value}
|
value={value}
|
||||||
inputref={ref}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
rows={1}
|
rows={1}
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
transition='none'
|
transition='none'
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
style={{ padding: 0 }}
|
style={{ padding: 0 }}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// events dont necessarily contain all custom fields
|
// events dont necessarily contain all custom fields
|
||||||
const initialValue = event.custom[column.id] ?? '';
|
const initialValue = event.custom[column.id]?.value ?? '';
|
||||||
|
|
||||||
return <EditableCell value={initialValue} handleUpdate={update} />;
|
return <EditableCell value={initialValue} handleUpdate={update} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, custo
|
|||||||
// for custom fields, we need to extract the value from the custom object
|
// for custom fields, we need to extract the value from the custom object
|
||||||
if (field.startsWith('custom-')) {
|
if (field.startsWith('custom-')) {
|
||||||
const fieldLabel = field.split('custom-')[1];
|
const fieldLabel = field.split('custom-')[1];
|
||||||
const value = entry.custom[fieldLabel];
|
const value = entry.custom[fieldLabel]?.value;
|
||||||
row.push(parseField(fieldLabel, value));
|
row.push(parseField(fieldLabel, value));
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error -- it is ok, we will just not have the data for other fields
|
// @ts-expect-error -- it is ok, we will just not have the data for other fields
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@use './EditorMixin' as editor;
|
@use './EditorMixin' as editor;
|
||||||
|
|
||||||
$min-playback-width: 27rem;
|
$min-playback-width: 27rem;
|
||||||
$max-playback-width: 30rem;
|
$max-playback-width: 35rem;
|
||||||
$panel-gap: 0.5rem;
|
$panel-gap: 0.5rem;
|
||||||
|
|
||||||
.corner {
|
.corner {
|
||||||
@@ -9,11 +9,11 @@ $panel-gap: 0.5rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mainContainer {
|
.mainContainer {
|
||||||
background-color: $ui-black;
|
background: $ui-black;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
padding: 0.5rem;
|
padding: 1rem 0.5rem;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto;
|
grid-template-columns: auto;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { lazy, useCallback, useEffect } from 'react';
|
import { lazy, useCallback, useEffect } from 'react';
|
||||||
import { IconButton, useDisclosure } from '@chakra-ui/react';
|
import { IconButton, useDisclosure } from '@chakra-ui/react';
|
||||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||||
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
|
||||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||||
|
|
||||||
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||||
@@ -9,7 +8,7 @@ import useElectronEvent from '../../common/hooks/useElectronEvent';
|
|||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import AppSettings from '../app-settings/AppSettings';
|
import AppSettings from '../app-settings/AppSettings';
|
||||||
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
import useAppSettingsNavigation from '../app-settings/useAppSettingsNavigation';
|
||||||
import { EditorOverview } from '../overview/Overview';
|
import Overview from '../overview/Overview';
|
||||||
|
|
||||||
import styles from './Editor.module.scss';
|
import styles from './Editor.module.scss';
|
||||||
|
|
||||||
@@ -63,19 +62,10 @@ export default function Editor() {
|
|||||||
|
|
||||||
useWindowTitle('Editor');
|
useWindowTitle('Editor');
|
||||||
|
|
||||||
// listen to shutdown request from electron process
|
|
||||||
useEffect(() => {
|
|
||||||
if (window.process?.type === 'renderer') {
|
|
||||||
window.ipcRenderer.on('user-request-shutdown', () => {
|
|
||||||
setLocation('shutdown');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [setLocation]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||||
<EditorOverview>
|
<Overview>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Toggle navigation'
|
aria-label='Toggle navigation'
|
||||||
variant='ontime-subtle-white'
|
variant='ontime-subtle-white'
|
||||||
@@ -85,12 +75,12 @@ export default function Editor() {
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Toggle settings'
|
aria-label='Toggle settings'
|
||||||
variant={isSettingsOpen ? 'ontime-subtle' : 'ontime-subtle-white'}
|
variant='ontime-subtle-white'
|
||||||
size='lg'
|
size='lg'
|
||||||
icon={isSettingsOpen ? <IoClose /> : <IoSettingsOutline />}
|
icon={<IoSettingsOutline />}
|
||||||
onClick={toggleSettings}
|
onClick={toggleSettings}
|
||||||
/>
|
/>
|
||||||
</EditorOverview>
|
</Overview>
|
||||||
{isSettingsOpen ? (
|
{isSettingsOpen ? (
|
||||||
<AppSettings />
|
<AppSettings />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
background-color: $ui-black;
|
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export default function Operator() {
|
|||||||
|
|
||||||
const mainField = main ? entry?.[main] || entry.title : entry.title;
|
const mainField = main ? entry?.[main] || entry.title : entry.title;
|
||||||
const secondaryField = getPropertyValue(entry, secondary) ?? '';
|
const secondaryField = getPropertyValue(entry, secondary) ?? '';
|
||||||
const subscribedData = entry.custom[subscribe];
|
const subscribedData = entry.custom[subscribe]?.value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OperatorEvent
|
<OperatorEvent
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { millisToString } from 'ontime-utils';
|
|
||||||
|
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import { useRuntimeOverview, useRuntimePlaybackOverview, useTimer } from '../../common/hooks/useSocket';
|
import { useRuntimeOverview, useRuntimePlaybackOverview } from '../../common/hooks/useSocket';
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import { enDash } from '../../common/utils/styleUtils';
|
import { enDash } from '../../common/utils/styleUtils';
|
||||||
|
|
||||||
@@ -11,9 +10,9 @@ import { calculateEndAndDaySpan, formatedTime, getOffsetText } from './overviewU
|
|||||||
|
|
||||||
import style from './Overview.module.scss';
|
import style from './Overview.module.scss';
|
||||||
|
|
||||||
export const EditorOverview = memo(_EditorOverview);
|
export default memo(Overview);
|
||||||
|
|
||||||
function _EditorOverview({ children }: { children: React.ReactNode }) {
|
function Overview({ children }: { children: React.ReactNode }) {
|
||||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
||||||
|
|
||||||
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
||||||
@@ -32,41 +31,6 @@ function _EditorOverview({ children }: { children: React.ReactNode }) {
|
|||||||
<TimeRow label='Planned start' value={formatedTime(plannedStart)} className={style.start} />
|
<TimeRow label='Planned start' value={formatedTime(plannedStart)} className={style.start} />
|
||||||
<TimeRow label='Actual start' value={formatedTime(actualStart)} className={style.start} />
|
<TimeRow label='Actual start' value={formatedTime(actualStart)} className={style.start} />
|
||||||
</div>
|
</div>
|
||||||
<ProgressOverview />
|
|
||||||
<RuntimeOverview />
|
|
||||||
<div>
|
|
||||||
<TimeRow label='Planned end' value={plannedEndText} className={style.end} daySpan={maybePlannedDaySpan} />
|
|
||||||
<TimeRow
|
|
||||||
label='Expected end'
|
|
||||||
value={expectedEndText}
|
|
||||||
className={style.end}
|
|
||||||
daySpan={maybeExpectedDaySpan}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CuesheetOverview = memo(_CuesheetOverview);
|
|
||||||
|
|
||||||
function _CuesheetOverview({ children }: { children: React.ReactNode }) {
|
|
||||||
const { plannedEnd, expectedEnd } = useRuntimeOverview();
|
|
||||||
|
|
||||||
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
|
||||||
const plannedEndText = formatedTime(maybePlannedEnd);
|
|
||||||
|
|
||||||
const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]);
|
|
||||||
const expectedEndText = formatedTime(maybeExpectedEnd);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.overview}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
<div className={style.nav}>{children}</div>
|
|
||||||
<div className={style.info}>
|
|
||||||
<TitlesOverview />
|
|
||||||
<TimerOverview />
|
|
||||||
<RuntimeOverview />
|
<RuntimeOverview />
|
||||||
<div>
|
<div>
|
||||||
<TimeRow label='Planned end' value={plannedEndText} className={style.end} daySpan={maybePlannedDaySpan} />
|
<TimeRow label='Planned end' value={plannedEndText} className={style.end} daySpan={maybePlannedDaySpan} />
|
||||||
@@ -94,32 +58,18 @@ function TitlesOverview() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimerOverview() {
|
function RuntimeOverview() {
|
||||||
const { current } = useTimer();
|
const { clock, numEvents, selectedEventIndex, offset } = useRuntimePlaybackOverview();
|
||||||
|
|
||||||
const display = millisToString(current);
|
|
||||||
|
|
||||||
return <TimeColumn label='Running timer' value={display} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProgressOverview() {
|
|
||||||
const { numEvents, selectedEventIndex } = useRuntimePlaybackOverview();
|
|
||||||
|
|
||||||
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
const current = selectedEventIndex !== null ? selectedEventIndex + 1 : enDash;
|
||||||
const ofTotal = numEvents || enDash;
|
const ofTotal = numEvents || enDash;
|
||||||
const progressText = numEvents ? `${current} of ${ofTotal}` : '-';
|
const progressText = numEvents ? `${current} of ${ofTotal}` : '-';
|
||||||
|
|
||||||
return <TimeColumn label='Progress' value={progressText} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function RuntimeOverview() {
|
|
||||||
const { clock, offset } = useRuntimePlaybackOverview();
|
|
||||||
|
|
||||||
const offsetText = getOffsetText(offset);
|
const offsetText = getOffsetText(offset);
|
||||||
const offsetClasses = offset === null ? undefined : offset <= 0 ? style.behind : style.ahead;
|
const offsetClasses = offset === null ? undefined : offset > 0 ? style.behind : style.ahead;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<TimeColumn label='Progress' value={progressText} />
|
||||||
<TimeColumn label='Offset' value={offsetText} className={offsetClasses} />
|
<TimeColumn label='Offset' value={offsetText} className={offsetClasses} />
|
||||||
<TimeColumn label='Time now' value={formatedTime(clock)} />
|
<TimeColumn label='Time now' value={formatedTime(clock)} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { dayInMs } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { calculateEndAndDaySpan } from '../overviewUtils';
|
|
||||||
|
|
||||||
describe('calculateEndAndDaySpan', () => {
|
|
||||||
it('should return [null, 0] when end is null', () => {
|
|
||||||
const result = calculateEndAndDaySpan(null);
|
|
||||||
expect(result).toEqual([null, 0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return [end, 0] when end is less than or equal to dayInMs', () => {
|
|
||||||
const end = dayInMs / 2;
|
|
||||||
const result = calculateEndAndDaySpan(end);
|
|
||||||
expect(result).toEqual([end, 0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return [end % dayInMs, Math.floor(end / dayInMs)] when end is greater than dayInMs', () => {
|
|
||||||
const end = dayInMs * 1.5;
|
|
||||||
const result = calculateEndAndDaySpan(end);
|
|
||||||
expect(result).toEqual([end % dayInMs, Math.floor(end / dayInMs)]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -13,14 +13,20 @@ export function formatedTime(time: MaybeNumber) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates how long a time is and how many days it spans
|
* Calculates a day span from a number range
|
||||||
|
* @param end
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function calculateEndAndDaySpan(end: MaybeNumber): [MaybeNumber, number] {
|
export function calculateEndAndDaySpan(end: MaybeNumber): [MaybeNumber, number] {
|
||||||
if (end !== null && end > dayInMs) {
|
let maybeEnd = end;
|
||||||
return [end % dayInMs, Math.floor(end / dayInMs)];
|
let maybeDaySpan = 0;
|
||||||
|
if (end !== null) {
|
||||||
|
if (end > dayInMs) {
|
||||||
|
maybeEnd = end % dayInMs;
|
||||||
|
maybeDaySpan = Math.floor(end / dayInMs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return [maybeEnd, maybeDaySpan];
|
||||||
return [end, 0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,5 +38,10 @@ export function getOffsetText(offset: MaybeNumber): string {
|
|||||||
if (offset === null) {
|
if (offset === null) {
|
||||||
return enDash;
|
return enDash;
|
||||||
}
|
}
|
||||||
return millisToString(offset, { fallback: enDash });
|
const isAhead = offset <= 0;
|
||||||
|
let offsetText = millisToString(Math.abs(offset), { fallback: enDash });
|
||||||
|
if (offsetText !== enDash) {
|
||||||
|
offsetText = isAhead ? `+${offsetText}` : `${enDash}${offsetText}`;
|
||||||
|
}
|
||||||
|
return offsetText;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rundownContainer {
|
.eventContainer {
|
||||||
margin-top: 1rem;
|
margin-top: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-right: 4px; // size of scrollbar
|
padding-right: 4px; // size of scrollbar
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { useHotkeys } from '@mantine/hooks';
|
import {
|
||||||
import { isOntimeEvent, MaybeNumber, Playback, RundownCached, SupportedEvent } from 'ontime-types';
|
isOntimeBlock,
|
||||||
import { getFirstNormal, getLastNormal, getNextNormal, getPreviousNormal } from 'ontime-utils';
|
isOntimeDelay,
|
||||||
|
isOntimeEvent,
|
||||||
|
MaybeNumber,
|
||||||
|
Playback,
|
||||||
|
RundownCached,
|
||||||
|
SupportedEvent,
|
||||||
|
} from 'ontime-types';
|
||||||
|
import { getFirstNormal, getNextNormal, getPreviousNormal } from 'ontime-utils';
|
||||||
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
import { useEditorSettings } from '../../common/stores/editorSettings';
|
||||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||||
|
|
||||||
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
||||||
@@ -28,9 +35,10 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
const [statefulEntries, setStatefulEntries] = useState(order);
|
const [statefulEntries, setStatefulEntries] = useState(order);
|
||||||
|
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
const { addEvent, reorderEvent, deleteEvent } = useEventAction();
|
const { addEvent, reorderEvent } = useEventAction();
|
||||||
|
const eventSettings = useEditorSettings((state) => state.eventSettings);
|
||||||
const { entryCopyId, setEntryCopyId } = useEntryCopy();
|
const defaultPublic = eventSettings.defaultPublic;
|
||||||
|
const linkPrevious = eventSettings.linkPrevious;
|
||||||
|
|
||||||
// cursor
|
// cursor
|
||||||
const { cursor, mode: appMode, setCursor } = useAppMode();
|
const { cursor, mode: appMode, setCursor } = useAppMode();
|
||||||
@@ -41,21 +49,9 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
// DND KIT
|
// DND KIT
|
||||||
const sensors = useSensors(useSensor(PointerSensor));
|
const sensors = useSensors(useSensor(PointerSensor));
|
||||||
|
|
||||||
const deleteAtCursor = useCallback(
|
|
||||||
(cursor: string | null) => {
|
|
||||||
if (!cursor) return;
|
|
||||||
const previous = getPreviousNormal(rundown, order, cursor).entry?.id ?? null;
|
|
||||||
deleteEvent(cursor);
|
|
||||||
setCursor(previous);
|
|
||||||
},
|
|
||||||
[deleteEvent, order, rundown, setCursor],
|
|
||||||
);
|
|
||||||
|
|
||||||
const insertAtCursor = useCallback(
|
const insertAtCursor = useCallback(
|
||||||
(type: SupportedEvent | 'clone', cursor: string | null, above = false) => {
|
(type: SupportedEvent | 'clone', cursor: string | null) => {
|
||||||
const adjustedCursor = above ? getPreviousNormal(rundown, order, cursor ?? '').entry?.id ?? null : cursor;
|
if (cursor === null) {
|
||||||
|
|
||||||
if (adjustedCursor === null) {
|
|
||||||
// we cant clone without selection
|
// we cant clone without selection
|
||||||
if (type === 'clone') {
|
if (type === 'clone') {
|
||||||
return;
|
return;
|
||||||
@@ -66,7 +62,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'clone') {
|
if (type === 'clone') {
|
||||||
const cursorEvent = rundown[adjustedCursor];
|
const cursorEvent = rundown[cursor];
|
||||||
if (cursorEvent?.type === SupportedEvent.Event) {
|
if (cursorEvent?.type === SupportedEvent.Event) {
|
||||||
const newEvent = cloneEvent(cursorEvent, cursorEvent.id);
|
const newEvent = cloneEvent(cursorEvent, cursorEvent.id);
|
||||||
addEvent(newEvent);
|
addEvent(newEvent);
|
||||||
@@ -76,90 +72,113 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
};
|
};
|
||||||
const options = {
|
const options = {
|
||||||
after: adjustedCursor,
|
after: cursor,
|
||||||
lastEventId: adjustedCursor,
|
defaultPublic,
|
||||||
|
lastEventId: cursor,
|
||||||
|
linkPrevious,
|
||||||
};
|
};
|
||||||
addEvent(newEvent, options);
|
addEvent(newEvent, options);
|
||||||
} else {
|
} else {
|
||||||
addEvent({ type }, { after: adjustedCursor });
|
addEvent({ type }, { after: cursor });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rundown, order, addEvent],
|
[addEvent, rundown, defaultPublic, linkPrevious],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectEntry = useCallback(
|
// Handle keyboard shortcuts
|
||||||
(cursor: string | null, direction: 'up' | 'down') => {
|
const handleKeyDown = useCallback(
|
||||||
if (order.length < 1) {
|
(event: KeyboardEvent) => {
|
||||||
return;
|
// handle held key
|
||||||
}
|
if (event.repeat) return;
|
||||||
let newCursor: string | undefined;
|
|
||||||
if (cursor === null) {
|
|
||||||
// there is no cursor, we select the first or last depending on direction if it exists
|
|
||||||
newCursor = direction === 'up' ? getLastNormal(rundown, order)?.id : getFirstNormal(rundown, order)?.id;
|
|
||||||
} else {
|
|
||||||
// otherwise we select the next or previous
|
|
||||||
newCursor =
|
|
||||||
direction === 'up'
|
|
||||||
? getPreviousNormal(rundown, order, cursor).entry?.id
|
|
||||||
: getNextNormal(rundown, order, cursor).entry?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newCursor) {
|
const modKeysAlt = event.altKey && !event.ctrlKey && !event.shiftKey;
|
||||||
setCursor(newCursor);
|
const modKeysCtrlAlt = event.altKey && event.ctrlKey && !event.shiftKey;
|
||||||
|
|
||||||
|
if (modKeysAlt) {
|
||||||
|
switch (event.code) {
|
||||||
|
case 'ArrowDown': {
|
||||||
|
if (order.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextEvent =
|
||||||
|
cursor === null ? getFirstNormal(rundown, order) : getNextNormal(rundown, order, cursor)?.nextEvent;
|
||||||
|
if (nextEvent) {
|
||||||
|
setCursor(nextEvent.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ArrowUp': {
|
||||||
|
if (order.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we check for this before
|
||||||
|
const previousEvent =
|
||||||
|
cursor === null
|
||||||
|
? getFirstNormal(rundown, order)
|
||||||
|
: getPreviousNormal(rundown, order, cursor).previousEvent;
|
||||||
|
if (previousEvent) {
|
||||||
|
setCursor(previousEvent.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'KeyE': {
|
||||||
|
event.preventDefault();
|
||||||
|
insertAtCursor(SupportedEvent.Event, cursor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'KeyD': {
|
||||||
|
event.preventDefault();
|
||||||
|
insertAtCursor(SupportedEvent.Delay, cursor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'KeyB': {
|
||||||
|
event.preventDefault();
|
||||||
|
insertAtCursor(SupportedEvent.Block, cursor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'KeyC': {
|
||||||
|
event.preventDefault();
|
||||||
|
insertAtCursor('clone', cursor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (modKeysCtrlAlt) {
|
||||||
|
if (order.length < 2 || cursor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Alt + Ctrl + Arrow Down
|
||||||
|
if (event.code == 'ArrowDown') {
|
||||||
|
const { nextEvent, nextIndex } = getNextNormal(rundown, order, cursor);
|
||||||
|
if (nextEvent && nextIndex !== null) {
|
||||||
|
reorderEvent(cursor, nextIndex - 1, nextIndex);
|
||||||
|
}
|
||||||
|
// Alt + Ctrl + Arrow Up
|
||||||
|
} else if (event.code == 'ArrowUp') {
|
||||||
|
const { previousEvent, previousIndex } = getPreviousNormal(rundown, order, cursor);
|
||||||
|
if (previousEvent && previousIndex !== null) {
|
||||||
|
reorderEvent(cursor, previousIndex + 1, previousIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[order, rundown, setCursor],
|
[order, cursor, rundown, setCursor, insertAtCursor, reorderEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const moveEntry = useCallback(
|
|
||||||
(cursor: string | null, direction: 'up' | 'down') => {
|
|
||||||
if (order.length < 2 || cursor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { index } =
|
|
||||||
direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
|
|
||||||
|
|
||||||
if (index !== null) {
|
|
||||||
const offsetIndex = direction === 'up' ? index + 1 : index - 1;
|
|
||||||
reorderEvent(cursor, offsetIndex, index);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[order, reorderEvent, rundown],
|
|
||||||
);
|
|
||||||
|
|
||||||
// shortcuts
|
|
||||||
useHotkeys([
|
|
||||||
['alt + ArrowDown', () => selectEntry(cursor, 'down'), { preventDefault: true }],
|
|
||||||
['alt + ArrowUp', () => selectEntry(cursor, 'up'), { preventDefault: true }],
|
|
||||||
['alt + mod + ArrowDown', () => moveEntry(cursor, 'down'), { preventDefault: true }],
|
|
||||||
['alt + mod + ArrowUp', () => moveEntry(cursor, 'up'), { preventDefault: true }],
|
|
||||||
|
|
||||||
['Escape', () => setCursor(null), { preventDefault: true }],
|
|
||||||
|
|
||||||
['mod + Backspace', () => deleteAtCursor(cursor), { preventDefault: true }],
|
|
||||||
|
|
||||||
['alt + E', () => insertAtCursor(SupportedEvent.Event, cursor), { preventDefault: true }],
|
|
||||||
['alt + shift + E', () => insertAtCursor(SupportedEvent.Event, cursor, true), { preventDefault: true }],
|
|
||||||
|
|
||||||
['alt + B', () => insertAtCursor(SupportedEvent.Block, cursor), { preventDefault: true }],
|
|
||||||
['alt + shift + B', () => insertAtCursor(SupportedEvent.Block, cursor, true), { preventDefault: true }],
|
|
||||||
|
|
||||||
['alt + D', () => insertAtCursor(SupportedEvent.Delay, cursor), { preventDefault: true }],
|
|
||||||
['alt + shift + D', () => insertAtCursor(SupportedEvent.Delay, cursor, true), { preventDefault: true }],
|
|
||||||
|
|
||||||
['mod + C', () => setEntryCopyId(cursor), { preventDefault: true }],
|
|
||||||
['mod + V', () => insertAtCursor('clone', entryCopyId), { preventDefault: true }],
|
|
||||||
['mod + shift + V', () => insertAtCursor('clone', entryCopyId, true), { preventDefault: true }],
|
|
||||||
|
|
||||||
['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true }],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// we copy the state from the store here
|
// we copy the state from the store here
|
||||||
// to workaround async updates on the drag mutations
|
// to workaround async updates on the drag mutations
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStatefulEntries(order);
|
setStatefulEntries(order);
|
||||||
}, [order]);
|
}, [order]);
|
||||||
|
|
||||||
|
// listen to keys
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [handleKeyDown]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// in run mode, we follow selection
|
// in run mode, we follow selection
|
||||||
if (appMode !== AppMode.Run || !featureData?.selectedEventId) {
|
if (appMode !== AppMode.Run || !featureData?.selectedEventId) {
|
||||||
@@ -202,7 +221,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
const isEditMode = appMode === AppMode.Edit;
|
const isEditMode = appMode === AppMode.Edit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
<div className={style.eventContainer} ref={scrollRef} data-testid='rundown'>
|
||||||
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
|
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
|
||||||
<SortableContext items={statefulEntries} strategy={verticalListSortingStrategy}>
|
<SortableContext items={statefulEntries} strategy={verticalListSortingStrategy}>
|
||||||
<div className={style.list}>
|
<div className={style.list}>
|
||||||
@@ -241,7 +260,14 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={event.id}>
|
<Fragment key={event.id}>
|
||||||
{isEditMode && (hasCursor || isFirst) && <QuickAddBlock previousEventId={previousEventId} />}
|
{isEditMode && (hasCursor || isFirst) && (
|
||||||
|
<QuickAddBlock
|
||||||
|
showKbd={hasCursor ? 'above' : 'none'}
|
||||||
|
previousEventId={previousEventId}
|
||||||
|
disableAddDelay={isOntimeDelay(event)}
|
||||||
|
disableAddBlock={isOntimeBlock(event)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className={style.entryWrapper} data-testid={`entry-${eventIndex}`}>
|
<div className={style.entryWrapper} data-testid={`entry-${eventIndex}`}>
|
||||||
{isOntimeEvent(event) && <div className={style.entryIndex}>{eventIndex}</div>}
|
{isOntimeEvent(event) && <div className={style.entryIndex}>{eventIndex}</div>}
|
||||||
<div className={style.entry} key={event.id} ref={hasCursor ? cursorRef : undefined}>
|
<div className={style.entry} key={event.id} ref={hasCursor ? cursorRef : undefined}>
|
||||||
@@ -261,7 +287,14 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isEditMode && (hasCursor || isLast) && <QuickAddBlock previousEventId={event.id} />}
|
{isEditMode && (hasCursor || isLast) && (
|
||||||
|
<QuickAddBlock
|
||||||
|
showKbd={hasCursor ? 'below' : 'none'}
|
||||||
|
previousEventId={event.id}
|
||||||
|
disableAddDelay={isOntimeDelay(event)}
|
||||||
|
disableAddBlock={isOntimeBlock(event)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { MaybeNumber, OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent
|
|||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
||||||
import { useAppMode } from '../../common/stores/appModeStore';
|
import { useAppMode } from '../../common/stores/appModeStore';
|
||||||
|
import { useEditorSettings } from '../../common/stores/editorSettings';
|
||||||
import { useEmitLog } from '../../common/stores/logger';
|
import { useEmitLog } from '../../common/stores/logger';
|
||||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||||
|
|
||||||
@@ -60,6 +61,10 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
const setCursor = useAppMode((state) => state.setCursor);
|
const setCursor = useAppMode((state) => state.setCursor);
|
||||||
const { selectedEvents, clearSelectedEvents } = useEventSelection();
|
const { selectedEvents, clearSelectedEvents } = useEventSelection();
|
||||||
|
|
||||||
|
const eventSettings = useEditorSettings((state) => state.eventSettings);
|
||||||
|
const defaultPublic = eventSettings.defaultPublic;
|
||||||
|
const linkPrevious = eventSettings.linkPrevious;
|
||||||
|
|
||||||
const removeOpenEvent = useCallback(() => {
|
const removeOpenEvent = useCallback(() => {
|
||||||
if (selectedEvents.has(data.id)) {
|
if (selectedEvents.has(data.id)) {
|
||||||
clearSelectedEvents();
|
clearSelectedEvents();
|
||||||
@@ -83,7 +88,9 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
const newEvent = { type: SupportedEvent.Event };
|
const newEvent = { type: SupportedEvent.Event };
|
||||||
const options = {
|
const options = {
|
||||||
after: data.id,
|
after: data.id,
|
||||||
|
defaultPublic,
|
||||||
lastEventId: previousEventId,
|
lastEventId: previousEventId,
|
||||||
|
linkPrevious,
|
||||||
};
|
};
|
||||||
return addEvent(newEvent, options);
|
return addEvent(newEvent, options);
|
||||||
}
|
}
|
||||||
@@ -91,6 +98,8 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
const newEvent = { type: SupportedEvent.Event };
|
const newEvent = { type: SupportedEvent.Event };
|
||||||
const options = {
|
const options = {
|
||||||
after: previousEventId,
|
after: previousEventId,
|
||||||
|
defaultPublic,
|
||||||
|
linkPrevious,
|
||||||
};
|
};
|
||||||
return addEvent(newEvent, options);
|
return addEvent(newEvent, options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */
|
flex: 1 2 auto; /* flex-grow: 1, flex-shrink: 2, flex-basis: auto */
|
||||||
min-width: 38rem;
|
min-width: 38rem;
|
||||||
max-width: 60rem;
|
max-width: 45rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side {
|
.side {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user