mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-02 22:18:08 +00:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 672267c0b3 | |||
| ca65d31071 | |||
| 305d6b6476 | |||
| 139b667e20 | |||
| 29a9167d61 | |||
| 38654f8981 | |||
| 33b496f98e | |||
| 10673b3ee1 | |||
| 4bceae65ab | |||
| 40d389acc9 | |||
| 0b94fc71a4 | |||
| 8f834fed35 | |||
| db9cb61164 | |||
| 9c4c84905e | |||
| da889a4825 | |||
| 4ca9fb9f72 | |||
| b228020b73 | |||
| f248b78945 | |||
| 11663e5416 | |||
| 6732314f15 | |||
| e9fe0c0a3a | |||
| 770d12888d | |||
| 94a1369d64 | |||
| 5ba27dca1d | |||
| 3df17d172f | |||
| d59c92219f | |||
| 23fce763e5 | |||
| 524aa74a3d | |||
| 32b7908d5b | |||
| e36b19a511 | |||
| ba62276951 | |||
| 4c4b83979d | |||
| 2dcea86b36 | |||
| e54996bd5e | |||
| 41753fc202 | |||
| e77572c0b6 | |||
| 4c268d531e | |||
| 5e481d49da | |||
| 0a68377b82 | |||
| cb39a13bea | |||
| e56460a940 | |||
| 390d466673 | |||
| b778835469 | |||
| 32b5ab0574 | |||
| 03e7428348 | |||
| 0e47b5c10c | |||
| 6582f4a9bc | |||
| 7456205b6e | |||
| 9338a615d4 | |||
| 03d5389540 | |||
| bca7ad30b1 | |||
| e937af62b1 | |||
| 73533600a0 | |||
| 76c8f8a4d5 | |||
| 11648ee546 | |||
| 5b977790b0 | |||
| 3e2626d4f9 | |||
| 99f4738f4a | |||
| c405698813 | |||
| 6d138ce9f1 | |||
| 068c72662d | |||
| edac1d7f75 | |||
| 03552056cb | |||
| 70cc071425 | |||
| d8a84ac88d | |||
| 63d763cbaf | |||
| df351b8cba | |||
| 298af3ec5a | |||
| de9a7a87fd | |||
| 3918758d32 | |||
| 832c060940 | |||
| 110d3fb7a3 | |||
| 062ff7226a | |||
| 6f634c36f9 | |||
| 1bb67eb82a | |||
| 6720626bd3 | |||
| c56c5a636d | |||
| d486d78594 | |||
| 3626b5b357 | |||
| b54c7dd454 | |||
| ed3554d033 | |||
| 139f466beb | |||
| 23b1ced3fb | |||
| 0f5747839c | |||
| ff5735fe36 | |||
| c1f87736eb | |||
| edd983dfd6 | |||
| 2a5ba90530 | |||
| c03ed07762 | |||
| a3f14182a4 | |||
| 918f8517d2 | |||
| 54442dc9a1 | |||
| 1b1aced296 | |||
| d8ee0d4f82 | |||
| 08d2ebcc5e | |||
| 0fea4064c3 | |||
| 13be6ea2bc | |||
| c2ed9d7634 | |||
| cd0577bf99 |
Binary file not shown.
|
After Width: | Height: | Size: 285 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 660 KiB |
@@ -0,0 +1,139 @@
|
||||
name: Ontime build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ "v1.*.*" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_mac:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install --frozen-lockfile --network-timeout 300000
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install --frozen-lockfile --production --network-timeout 300000
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-mac
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-macOS.dmg
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build_win:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install --frozen-lockfile --network-timeout 300000
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install --frozen-lockfile --production --network-timeout 300000
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-win
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-win64.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build_linux:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install --frozen-lockfile --network-timeout 300000
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install --frozen-lockfile --production --network-timeout 300000
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-linux
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-linux.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -2,7 +2,7 @@ name: Docker Image CI Ontime V2
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ "*" ]
|
||||
tags: [ "v2.*.*" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -52,5 +52,5 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
# Push is a shorthand for --output=type=registry
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:beta_${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:beta_v2
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Ontime build v2
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ "*" ]
|
||||
tags: [ "v2.*.*" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: ontime_test_CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
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: 16
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Run tests
|
||||
run: yarn test:pipeline
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
run: yarn setup
|
||||
working-directory: ./server
|
||||
|
||||
- name: Server - run tests
|
||||
run: yarn test
|
||||
working-directory: ./server
|
||||
|
||||
# - name: Install Playwright Browsers
|
||||
# run: npx playwright install --with-deps
|
||||
# working-directory: ./server
|
||||
#
|
||||
# - name: Run Playwright tests
|
||||
# run: yarn e2e
|
||||
# working-directory: ./server
|
||||
#
|
||||
# - uses: actions/upload-artifact@v3
|
||||
# if: always()
|
||||
# with:
|
||||
# name: playwright-report
|
||||
# path: playwright-report/
|
||||
# retention-days: 7
|
||||
@@ -2,7 +2,7 @@ name: Ontime test v2
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: '*'
|
||||
branches: [ v2 ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
[](https://github.com/cpvalente/ontime/actions/workflows/build_v2.yml)
|
||||
[](https://github.com/cpvalente/ontime/actions/workflows/ontime_cy.yml) [](https://github.com/cpvalente/ontime/actions/workflows/build.yml)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://ontime.gitbook.io)
|
||||
|
||||
## Download the latest releases here
|
||||
|
||||
<div style="display: flex; justify-content: space-around">
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/mac-download.png"/></a>
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/win-download.png"/></a>
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/linux-download.png"/></a>
|
||||
<a href="https://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://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/mac-download.png"/></a>
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/win-download.png"/></a>
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/></a>
|
||||
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/dockerhub.png"/></a>
|
||||
</div>
|
||||
|
||||
# Ontime
|
||||
|
||||
Ontime is an application for creating and managing event running order and timers.
|
||||
Ontime is an application for managing event rundowns and running stage timers.
|
||||
|
||||
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.
|
||||
A single, locally hosted central application distributes your event information over the local network.
|
||||
This enables the distribution of the data to a series of views and allows integration into video and control workflows, including OBS and d3.
|
||||
|
||||
This makes for a simple and cheap way to distribute over a venue using a network infrastructure instead of video outputs.
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## Using Ontime
|
||||
|
||||
@@ -32,8 +30,8 @@ Any device with a browser in the same network can choose one of the supported vi
|
||||
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.
|
||||
You can then use the Ontime logo in the top left corner to select the desired view.
|
||||
The logo will be initially hidden until there is mouse interaction.
|
||||
|
||||
In the case of unattended machines or automation, it is possible to use different URL to recall
|
||||
individual views and extend view settings using the URL aliases feature
|
||||
@@ -64,23 +62,21 @@ More documentation is available [in our docs](https://ontime.gitbook.io)
|
||||
- [x] Distribute data over network and render it in the browser
|
||||
- [x] Different screen types
|
||||
- Stage Timer
|
||||
- Minimal Timer
|
||||
- Clock
|
||||
- Backstage Info
|
||||
- Public Info
|
||||
- Studio Clock
|
||||
- Countdown
|
||||
- [Make your own?](#make-your-own-viewer)
|
||||
- [x] Configurable Lower Thirds
|
||||
- [x] Collaborative editing with the cuesheet view
|
||||
- [x] Cuesheets with user definable fields
|
||||
- [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
|
||||
- [x] Protocol integrations for Control and Feedback
|
||||
- OSC (Open Sound Control)
|
||||
- HTTP
|
||||
- WebSockets
|
||||
- [x] Roll mode: run standalone using the system clock
|
||||
- [x] Roll mode: run independently 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://ontime.gitbook.io/v2/views/countdown): have
|
||||
a countdown to any scheduled event
|
||||
@@ -89,11 +85,14 @@ More documentation is available [in our docs](https://ontime.gitbook.io)
|
||||
|
||||
## Unopinionated
|
||||
|
||||
We want Ontime to be unique by targeting freelancers instead of roles.
|
||||
We are not interested in forcing workflows and have made Ontime, so it is flexible to whichever way
|
||||
you would like to work.
|
||||
|
||||
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.
|
||||
- [x] If you want just the info screens, there is no need to use the timer!
|
||||
- [x] Don't have or care for a schedule?
|
||||
- [x] a single event with no data is enough to use one of the APIs and use a dynamic timer
|
||||
- [x] use the order list to create a set of quick timers by setting the beginning and start
|
||||
times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC or any of the other available integrations
|
||||
|
||||
## Rich APIs for workflow integrations
|
||||
|
||||
@@ -117,7 +116,7 @@ Writing a new view for the browser can be done with basic knowledge of HTML + CS
|
||||
<br />
|
||||
See [this repository](https://github.com/cpvalente/ontime-viewer-template-v2) with a small template on
|
||||
how to get you started and read the docs about
|
||||
the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/ontime-apis#osc-and-websocket-api)
|
||||
the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/websocket-api)
|
||||
|
||||
### Headless run️
|
||||
|
||||
@@ -140,8 +139,6 @@ and use the included docker compose to get started
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Related information available [in the docs](https://ontime.gitbook.io/v2/additional-notes/use-in-raspberry-pi)
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Continued development
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||||
<meta name='theme-color' content='#121212' />
|
||||
<meta name='ontime' content='ontime - event timer manager' />
|
||||
<meta http-equiv="Content-Security-Policy" content="fullscreen 'self';">
|
||||
<link rel='apple-touch-icon' href='/logo192.png' />
|
||||
<link
|
||||
rel='icon'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "2.0.8",
|
||||
"version": "2.0.0-beta",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.5.5",
|
||||
|
||||
+22
-23
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
@@ -6,7 +6,6 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
||||
import { AppContextProvider } from './common/context/AppContext';
|
||||
import { ContextMenuProvider } from './common/context/ContextMenuContext';
|
||||
import useElectronEvent from './common/hooks/useElectronEvent';
|
||||
import { ontimeQueryClient } from './common/queryClient';
|
||||
import { connectSocket } from './common/utils/socket';
|
||||
@@ -23,19 +22,19 @@ connectSocket();
|
||||
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');
|
||||
}
|
||||
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');
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron) {
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
@@ -44,24 +43,24 @@ function App() {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
};
|
||||
}, [isElectron, sendToElectron]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ChakraProvider resetCSS theme={theme}>
|
||||
<QueryClientProvider client={ontimeQueryClient}>
|
||||
<AppContextProvider>
|
||||
<ContextMenuProvider>
|
||||
<BrowserRouter>
|
||||
<div className='App'>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<div className='App'>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<TranslationProvider>
|
||||
<AppRouter />
|
||||
</TranslationProvider>
|
||||
</ErrorBoundary>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</ContextMenuProvider>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</AppContextProvider>
|
||||
</QueryClientProvider>
|
||||
</ChakraProvider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lazy, Suspense, useEffect } from 'react';
|
||||
import { lazy, useEffect } from 'react';
|
||||
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import useAliases from './common/hooks-query/useAliases';
|
||||
@@ -50,72 +50,70 @@ export default function AppRouter() {
|
||||
}, [data, location, navigate]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<Routes>
|
||||
<Route path='/' element={<Navigate to='/timer' />} />
|
||||
<Route path='/speaker' element={<STimer />} />
|
||||
<Route path='/presenter' element={<STimer />} />
|
||||
<Route path='/stage' element={<STimer />} />
|
||||
<Route path='/timer' element={<STimer />} />
|
||||
<Routes>
|
||||
<Route path='/' element={<Navigate to='/timer' />} />
|
||||
<Route path='/speaker' element={<STimer />} />
|
||||
<Route path='/presenter' element={<STimer />} />
|
||||
<Route path='/stage' element={<STimer />} />
|
||||
<Route path='/timer' element={<STimer />} />
|
||||
|
||||
<Route path='/minimal' element={<SMinimalTimer />} />
|
||||
<Route path='/minimalTimer' element={<SMinimalTimer />} />
|
||||
<Route path='/simpleTimer' element={<SMinimalTimer />} />
|
||||
<Route path='/minimal' element={<SMinimalTimer />} />
|
||||
<Route path='/minimalTimer' element={<SMinimalTimer />} />
|
||||
<Route path='/simpleTimer' element={<SMinimalTimer />} />
|
||||
|
||||
<Route path='/clock' element={<SClock />} />
|
||||
<Route path='/clock' element={<SClock />} />
|
||||
|
||||
<Route path='/countdown' element={<SCountdown />} />
|
||||
<Route path='/countdown' element={<SCountdown />} />
|
||||
|
||||
<Route path='/sm' element={<SBackstage />} />
|
||||
<Route path='/backstage' element={<SBackstage />} />
|
||||
<Route path='/sm' element={<SBackstage />} />
|
||||
<Route path='/backstage' element={<SBackstage />} />
|
||||
|
||||
<Route path='/public' element={<SPublic />} />
|
||||
<Route path='/studio' element={<SStudio />} />
|
||||
{/*/!* Lower cannot have fallback *!/*/}
|
||||
<Route path='/lower' element={<SLowerThird />} />
|
||||
<Route path='/public' element={<SPublic />} />
|
||||
<Route path='/studio' element={<SStudio />} />
|
||||
{/*/!* Lower cannot have fallback *!/*/}
|
||||
<Route path='/lower' element={<SLowerThird />} />
|
||||
|
||||
{/*/!* Protected Routes *!/*/}
|
||||
<Route path='/editor' element={<Editor />} />
|
||||
<Route path='/cuesheet' element={<Table />} />
|
||||
<Route path='/cuelist' element={<Table />} />
|
||||
<Route path='/table' element={<Table />} />
|
||||
{/*/!* Protected Routes *!/*/}
|
||||
<Route path='/editor' element={<Editor />} />
|
||||
<Route path='/cuesheet' element={<Table />} />
|
||||
<Route path='/cuelist' element={<Table />} />
|
||||
<Route path='/table' element={<Table />} />
|
||||
|
||||
{/*/!* Protected Routes - Elements *!/*/}
|
||||
<Route
|
||||
path='/rundown'
|
||||
element={
|
||||
<EditorFeatureWrapper>
|
||||
<RundownPanel />
|
||||
</EditorFeatureWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/timercontrol'
|
||||
element={
|
||||
<EditorFeatureWrapper>
|
||||
<TimerControl />
|
||||
</EditorFeatureWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/messagecontrol'
|
||||
element={
|
||||
<EditorFeatureWrapper>
|
||||
<MessageControl />
|
||||
</EditorFeatureWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/info'
|
||||
element={
|
||||
<EditorFeatureWrapper>
|
||||
<Info />
|
||||
</EditorFeatureWrapper>
|
||||
}
|
||||
/>
|
||||
{/*/!* Send to default if nothing found *!/*/}
|
||||
<Route path='*' element={<STimer />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
{/*/!* Protected Routes - Elements *!/*/}
|
||||
<Route
|
||||
path='/rundown'
|
||||
element={
|
||||
<EditorFeatureWrapper>
|
||||
<RundownPanel />
|
||||
</EditorFeatureWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/timercontrol'
|
||||
element={
|
||||
<EditorFeatureWrapper>
|
||||
<TimerControl />
|
||||
</EditorFeatureWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/messagecontrol'
|
||||
element={
|
||||
<EditorFeatureWrapper>
|
||||
<MessageControl />
|
||||
</EditorFeatureWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/info'
|
||||
element={
|
||||
<EditorFeatureWrapper>
|
||||
<Info />
|
||||
</EditorFeatureWrapper>
|
||||
}
|
||||
/>
|
||||
{/*/!* Send to default if nothing found *!/*/}
|
||||
<Route path='*' element={<STimer />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const queryClientMock = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
// Exported viewer link location
|
||||
const minimalLocation = 'minimal';
|
||||
const speakerLocation = 'speaker';
|
||||
const smLocation = 'sm';
|
||||
const publicLocation = 'public';
|
||||
const studioLocation = 'studio';
|
||||
const cuesheetLocation = 'cuesheet';
|
||||
const countdownLocation = 'countdown';
|
||||
const clockLocation = 'clock';
|
||||
const lowerLocation = 'lower';
|
||||
|
||||
export const viewerLocations = [
|
||||
{ link: speakerLocation, label: 'Stage timer' },
|
||||
{ link: clockLocation, label: 'Clock' },
|
||||
{ link: minimalLocation, label: 'Minimal timer' },
|
||||
{ link: smLocation, label: 'Backstage screen' },
|
||||
{ link: publicLocation, label: 'Public screen' },
|
||||
{ link: lowerLocation, label: 'Lower thirds' },
|
||||
{ link: studioLocation, label: 'Studio clock' },
|
||||
{ link: countdownLocation, label: 'Countdown' },
|
||||
{ link: cuesheetLocation, label: 'Cuesheet' },
|
||||
];
|
||||
@@ -14,7 +14,7 @@ export const RUNTIME = ['runtimeStore'];
|
||||
|
||||
export const serverPort = import.meta.env.DEV ? STATIC_PORT : window.location.port;
|
||||
export const serverURL = import.meta.env.DEV ? `http://localhost:${serverPort}` : window.location.origin;
|
||||
export const websocketUrl = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.hostname}:${serverPort}/ws`;
|
||||
export const websocketUrl = `ws://${window.location.hostname}:${serverPort}/ws`;
|
||||
|
||||
export const eventURL = `${serverURL}/eventdata`;
|
||||
export const rundownURL = `${serverURL}/events`;
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Alias,
|
||||
EventData,
|
||||
OSCSettings,
|
||||
OscSubscription,
|
||||
Settings,
|
||||
UserFields,
|
||||
ViewSettings,
|
||||
} from 'ontime-types';
|
||||
import { Alias, EventData, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { apiRepoLatest } from '../../externals';
|
||||
import { InfoType } from '../models/Info';
|
||||
@@ -108,14 +100,6 @@ export async function postOSC(data: OSCSettings) {
|
||||
return axios.post(`${ontimeURL}/osc`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate osc subscriptions
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function postOscSubscriptions(data: OscSubscription) {
|
||||
return axios.post(`${ontimeURL}/osc-subscriptions`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to download db
|
||||
* @return {Promise}
|
||||
@@ -152,7 +136,7 @@ export const downloadRundown = async () => {
|
||||
type UploadDataOptions = {
|
||||
onlyRundown?: boolean;
|
||||
};
|
||||
export const uploadData = async (file: File, setProgress: (value: number) => void, options?: UploadDataOptions) => {
|
||||
export const uploadData = async (file: string, setProgress: (value: number) => void, options?: UploadDataOptions) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file);
|
||||
const onlyRundown = options?.onlyRundown || 'false';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
|
||||
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function EnableBtn(props) {
|
||||
const { active, text, actionHandler, size = 'xs' } = props;
|
||||
return (
|
||||
<Button
|
||||
size={size}
|
||||
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={actionHandler}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
EnableBtn.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
text: PropTypes.string,
|
||||
actionHandler: PropTypes.func,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
interface PauseIconBtnProps {
|
||||
clickhandler: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
active: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export default function PauseIconBtn(props: PauseIconBtnProps) {
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Pause timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPause size='24px' />}
|
||||
colorScheme='orange'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
disabled={disabled}
|
||||
aria-label='Pause playback'
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function PublicIconBtn(props) {
|
||||
const { actionHandler, active, size = 'xs', ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
|
||||
<IconButton
|
||||
size={size}
|
||||
icon={<FiUsers />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
PublicIconBtn.propTypes = {
|
||||
actionHandler: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function RollIconBtn(props) {
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Roll mode' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoTimeOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
RollIconBtn.propTypes = {
|
||||
clickhandler: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function StartIconBtn(props) {
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Start timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlay size='24px' />}
|
||||
colorScheme='green'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
StartIconBtn.propTypes = {
|
||||
clickhandler: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { MouseEvent } from 'react';
|
||||
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
|
||||
|
||||
interface TooltipActionBtnProps extends IconButtonProps {
|
||||
clickHandler: (event?: MouseEvent) => void | Promise<void>;
|
||||
clickHandler: (event?: MouseEvent) => void;
|
||||
tooltip: string;
|
||||
openDelay?: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function TransportIconBtn(props) {
|
||||
const { clickHandler, icon, tooltip, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={tooltip} openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={icon}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
_hover={!disabled && { bg: '#ebedf0', color: '#333' }}
|
||||
onClick={clickHandler}
|
||||
width={90}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
TransportIconBtn.propTypes = {
|
||||
clickHandler: PropTypes.func,
|
||||
icon: PropTypes.element,
|
||||
tooltip: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function UnloadIconBtn(props) {
|
||||
const { clickHandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Unload event' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoStop size='22px' />}
|
||||
colorScheme='red'
|
||||
variant='outline'
|
||||
onClick={clickHandler}
|
||||
width={90}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
UnloadIconBtn.propTypes = {
|
||||
clickHandler: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import { IoCopy } from '@react-icons/all-files/io5/IoCopy';
|
||||
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
import { Size } from '../../models/Util.type';
|
||||
import copyToClipboard from '../../utils/copyToClipboard';
|
||||
|
||||
interface CopyTagProps {
|
||||
label: string;
|
||||
@@ -15,7 +14,10 @@ interface CopyTagProps {
|
||||
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
|
||||
const { label, className, size = 'xs', children } = props;
|
||||
|
||||
const handleClick = () => copyToClipboard(children as string);
|
||||
const handleClick = () => {
|
||||
// we need to this as a promise because safari
|
||||
setTimeout(async () => await navigator.clipboard.writeText(children as string));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={label} openDelay={tooltipDelayFast}>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
// skipcq: JS-C1003 - sentry does not expose itself as an ES Module.
|
||||
import * as Sentry from '@sentry/react';
|
||||
|
||||
import { runtime } from '@/common/stores/runtime';
|
||||
import { hasConnected, reconnectAttempts, shouldReconnect } from '@/common/utils/socket';
|
||||
|
||||
import style from './ErrorBoundary.module.scss';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
@@ -28,9 +24,7 @@ class ErrorBoundary extends React.Component {
|
||||
});
|
||||
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setExtras('error', error);
|
||||
scope.setExtras('store', runtime.getState());
|
||||
scope.setExtras('hasSocket', { hasConnected, shouldReconnect, reconnectAttempts });
|
||||
scope.setExtras(error);
|
||||
const eventId = Sentry.captureException(error);
|
||||
this.setState({ eventId, info });
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import { TimeEntryField } from '../../../utils/timesManager';
|
||||
import style from './TimeInput.module.scss';
|
||||
|
||||
interface TimeInputProps {
|
||||
id?: TimeEntryField;
|
||||
name: TimeEntryField;
|
||||
submitHandler: (field: TimeEntryField, value: number) => void;
|
||||
time?: number;
|
||||
@@ -37,7 +36,7 @@ function ButtonTooltip(name: TimeEntryField, warning?: string) {
|
||||
}
|
||||
|
||||
export default function TimeInput(props: TimeInputProps) {
|
||||
const { id, name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
|
||||
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [value, setValue] = useState<string>('');
|
||||
@@ -190,7 +189,6 @@ export default function TimeInput(props: TimeInputProps) {
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
data-testid='time-input'
|
||||
className={style.inputField}
|
||||
type='text'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
$progress-bar-size: 12px;
|
||||
$progress-bar-br: 3px;
|
||||
|
||||
.multiprogress-bar {
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: $progress-bar-size;
|
||||
@@ -17,7 +17,7 @@ $progress-bar-br: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.multiprogress-bar__indicator {
|
||||
.progress-bar__indicator {
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
background-color: black;
|
||||
@@ -27,7 +27,7 @@ $progress-bar-br: 3px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.multiprogress-bar__bg-normal {
|
||||
.progress-bar__bg-normal {
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
right: 0;
|
||||
@@ -35,14 +35,14 @@ $progress-bar-br: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.multiprogress-bar__bg-warning {
|
||||
.progress-bar__bg-warning {
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
right: 0;
|
||||
border-radius: 0 $progress-bar-br $progress-bar-br 0;
|
||||
}
|
||||
|
||||
.multiprogress-bar__bg-danger {
|
||||
.progress-bar__bg-danger {
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
right: 0;
|
||||
|
||||
@@ -23,11 +23,11 @@ export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
||||
const warningWidth = clamp((warning / complete) * 100, 0, 100);
|
||||
|
||||
return (
|
||||
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
|
||||
<div className='multiprogress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
|
||||
<div className='multiprogress-bar__bg-warning' style={{ width: `${warningWidth}%`, backgroundColor: warningColor }} />
|
||||
<div className='multiprogress-bar__bg-danger' style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }} />
|
||||
<div className='multiprogress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
||||
<div className={`progress-bar ${hidden ? 'progress-bar--hidden' : ''} ${className}`}>
|
||||
<div className='progress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
|
||||
<div className='progress-bar__bg-warning' style={{ width: `${warningWidth}%`, backgroundColor: warningColor }} />
|
||||
<div className='progress-bar__bg-danger' style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }} />
|
||||
<div className='progress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use '../../../theme/v2Styles' as *;
|
||||
@use '../../../theme/mixins' as *;
|
||||
@use '../../../theme/ontimeColours' as *;
|
||||
@use "../../../theme/v2Styles" as *;
|
||||
@use "../../../theme/mixins" as *;
|
||||
@use "../../../theme/ontimeColours" as *;
|
||||
|
||||
$menu-bg: $gray-1200;
|
||||
$menu-hover-bg: $gray-1350;
|
||||
@@ -14,27 +14,14 @@ $button-size: 48px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
padding: 0.5em;
|
||||
|
||||
.navButton {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
left: 0.5em;
|
||||
top: 0.5em;
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.3s;
|
||||
|
||||
opacity: 1;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 24px;
|
||||
color: $icon-color;
|
||||
background-color: $button-bg;
|
||||
@@ -43,11 +30,10 @@ $button-size: 48px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
@extend .button;
|
||||
z-index: 3;
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menuContainer {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import { IoPencilSharp } from '@react-icons/all-files/io5/IoPencilSharp';
|
||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
@@ -22,13 +21,12 @@ export default function NavigationMenu() {
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
||||
const [showButton, setShowButton] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
useClickOutside(menuRef, () => setShowMenu(false));
|
||||
|
||||
const toggleMenu = () => setShowMenu((prev) => !prev);
|
||||
useKeyDown(toggleMenu, ' ', { isDisabled: searchParams.get('edit') === 'true' });
|
||||
useKeyDown(toggleMenu, ' ');
|
||||
|
||||
useEffect(() => {
|
||||
let fadeOut: NodeJS.Timeout | null = null;
|
||||
@@ -51,68 +49,63 @@ export default function NavigationMenu() {
|
||||
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
|
||||
const handleFullscreen = () => toggleFullScreen();
|
||||
const handleMirror = () => toggleMirror();
|
||||
const showEditFormDrawer = () => {
|
||||
searchParams.append('edit', 'true');
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
|
||||
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
|
||||
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
|
||||
<IoApps />
|
||||
</button>
|
||||
<button className={style.button} onClick={showEditFormDrawer}>
|
||||
<IoPencilSharp />
|
||||
</button>
|
||||
{showMenu && (
|
||||
<div className={style.menuContainer} data-testid='navigation-menu'>
|
||||
<div className={style.buttonsContainer}>
|
||||
<div
|
||||
className={style.link}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
onClick={handleFullscreen}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && handleFullscreen();
|
||||
}}
|
||||
>
|
||||
Toggle Fullscreen
|
||||
{isFullScreen ? <IoContract /> : <IoExpand />}
|
||||
</div>
|
||||
<div
|
||||
className={style.link}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
onClick={handleMirror}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && handleMirror();
|
||||
}}
|
||||
>
|
||||
Flip Screen
|
||||
<IoSwapVertical />
|
||||
</div>
|
||||
{/*<div className={style.link} tabIndex={0}>*/}
|
||||
{/* Rename Client*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
<hr className={style.separator} />
|
||||
{navigatorConstants.map((route) => (
|
||||
<Link
|
||||
key={route.url}
|
||||
to={route.url}
|
||||
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
||||
tabIndex={0}
|
||||
>
|
||||
{route.label}
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
aria-label='toggle menu'
|
||||
className={`${style.navButton} ${!showButton && !showMenu ? style.hidden : ''}`}
|
||||
>
|
||||
<IoApps />
|
||||
</button>
|
||||
|
||||
{showMenu && (
|
||||
<div className={style.menuContainer} data-testid='navigation-menu'>
|
||||
<div className={style.buttonsContainer}>
|
||||
<div
|
||||
className={style.link}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
onClick={handleFullscreen}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && handleFullscreen();
|
||||
}}
|
||||
>
|
||||
Toggle Fullscreen
|
||||
{isFullScreen ? <IoContract /> : <IoExpand />}
|
||||
</div>
|
||||
<div
|
||||
className={style.link}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
onClick={handleMirror}
|
||||
onKeyDown={(event) => {
|
||||
isKeyEnter(event) && handleMirror();
|
||||
}}
|
||||
>
|
||||
Flip Screen
|
||||
<IoSwapVertical />
|
||||
</div>
|
||||
{/*<div className={style.link} tabIndex={0}>*/}
|
||||
{/* Rename Client*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
<hr className={style.separator} />
|
||||
{navigatorConstants.map((route) => (
|
||||
<Link
|
||||
key={route.url}
|
||||
to={route.url}
|
||||
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
||||
tabIndex={0}
|
||||
>
|
||||
{route.label}
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ $progress-bar-br: 6px;
|
||||
|
||||
.progress-bar__indicator {
|
||||
height: $progress-bar-size;
|
||||
border-radius: $progress-bar-br;
|
||||
background-color: var(--timer-progress-override, $accent-color);
|
||||
transition: 1s linear;
|
||||
border-radius: $progress-bar-br 0 0 $progress-bar-br;
|
||||
transition-property: width;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ interface ScheduleProps {
|
||||
}
|
||||
|
||||
export default function Schedule({ className }: ScheduleProps) {
|
||||
const { paginatedEvents, selectedEventId, isBackstage, scheduleType } = useSchedule();
|
||||
const { paginatedEvents, selectedEventId, isBackstage } = useSchedule();
|
||||
|
||||
if (paginatedEvents?.length < 1) {
|
||||
return <Empty text='No events to show' />;
|
||||
@@ -21,14 +21,10 @@ export default function Schedule({ className }: ScheduleProps) {
|
||||
return (
|
||||
<ul className={`schedule ${className}`}>
|
||||
{paginatedEvents.map((event) => {
|
||||
if (scheduleType === 'past' || scheduleType === 'future') {
|
||||
selectedState = scheduleType;
|
||||
} else {
|
||||
if (event.id === selectedEventId) {
|
||||
selectedState = 'now';
|
||||
} else if (selectedState === 'now') {
|
||||
selectedState = 'future';
|
||||
}
|
||||
if (event.id === selectedEventId) {
|
||||
selectedState = 'now';
|
||||
} else if (selectedState === 'now') {
|
||||
selectedState = 'future';
|
||||
}
|
||||
return (
|
||||
<ScheduleItem
|
||||
|
||||
@@ -7,7 +7,6 @@ interface ScheduleContextState {
|
||||
events: OntimeEvent[];
|
||||
paginatedEvents: OntimeEvent[];
|
||||
selectedEventId: string | null;
|
||||
scheduleType: 'past' | 'now' | 'future';
|
||||
numPages: number;
|
||||
visiblePage: number;
|
||||
isBackstage: boolean;
|
||||
@@ -28,7 +27,7 @@ export const ScheduleProvider = ({
|
||||
events,
|
||||
selectedEventId,
|
||||
isBackstage = false,
|
||||
eventsPerPage = 7,
|
||||
eventsPerPage = 8,
|
||||
time = 10,
|
||||
}: PropsWithChildren<ScheduleProviderProps>) => {
|
||||
const [visiblePage, setVisiblePage] = useState(0);
|
||||
@@ -37,18 +36,6 @@ export const ScheduleProvider = ({
|
||||
const eventStart = eventsPerPage * visiblePage;
|
||||
const eventEnd = eventsPerPage * (visiblePage + 1);
|
||||
const paginatedEvents = events.slice(eventStart, eventEnd);
|
||||
const selectedEventIndex = events.findIndex((event) => event.id === selectedEventId);
|
||||
|
||||
const resolveScheduleType = () => {
|
||||
if (selectedEventIndex >= eventStart && selectedEventIndex < eventEnd) {
|
||||
return 'now';
|
||||
}
|
||||
if (selectedEventIndex > eventEnd) {
|
||||
return 'past';
|
||||
}
|
||||
return 'future';
|
||||
};
|
||||
const scheduleType = resolveScheduleType();
|
||||
|
||||
// every SCROLL_TIME go to the next array
|
||||
useInterval(() => {
|
||||
@@ -64,7 +51,6 @@ export const ScheduleProvider = ({
|
||||
events,
|
||||
paginatedEvents,
|
||||
selectedEventId,
|
||||
scheduleType,
|
||||
numPages,
|
||||
visiblePage,
|
||||
isBackstage,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 600;
|
||||
font-size: 3.5rem;
|
||||
font-size: 3.75em;
|
||||
|
||||
&--finished {
|
||||
color: $timer-finished-color;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
import { formatDisplay } from 'ontime-utils';
|
||||
|
||||
import { formatDisplay } from '../../utils/dateConfig';
|
||||
|
||||
import './TimerDisplay.scss';
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
@use '../../../theme/v2Styles' as *;
|
||||
|
||||
.modalBody {
|
||||
min-height: 40vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.options {
|
||||
margin-bottom: 1.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.notes {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: $bg-container-onlight;
|
||||
margin: 1em 0;
|
||||
padding: 0.5em;
|
||||
border-radius: 2px;
|
||||
color: $text-black;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.corner {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.infoList {
|
||||
font-size: 0.9em;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.flexColumnLeft {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { ChangeEvent, useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Progress,
|
||||
} from '@chakra-ui/react';
|
||||
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { RUNDOWN_TABLE } from '../../api/apiConstants';
|
||||
import { uploadData } from '../../api/ontimeApi';
|
||||
import { useEmitLog } from '../../stores/logger';
|
||||
import TooltipActionBtn from '../buttons/TooltipActionBtn';
|
||||
|
||||
import { validateFile } from './utils';
|
||||
|
||||
import style from './UploadModal.module.scss';
|
||||
|
||||
interface UploadModalProps {
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { emitError } = useEmitLog();
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const overrideOptionRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFile = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const fileUploaded = event?.target?.files?.[0];
|
||||
if (!fileUploaded) return;
|
||||
|
||||
const validate = validateFile(fileUploaded);
|
||||
setErrors(validate.errors);
|
||||
|
||||
if (validate.isValid) {
|
||||
setFile(fileUploaded);
|
||||
} else {
|
||||
setFile(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleUpload = useCallback(async () => {
|
||||
if (file) {
|
||||
try {
|
||||
await uploadData(file, setProgress, { onlyEvents: overrideOptionRef?.current?.checked });
|
||||
} catch (error) {
|
||||
emitError(`Failed uploading file: ${error}`);
|
||||
} finally {
|
||||
await queryClient.invalidateQueries(RUNDOWN_TABLE);
|
||||
setFile(null);
|
||||
}
|
||||
}
|
||||
}, [emitError, file, queryClient]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
closeOnOverlayClick={false}
|
||||
motionPreset='slideInBottom'
|
||||
size='xl'
|
||||
scrollBehavior='inside'
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>File upload</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody className={style.modalBody}>
|
||||
<FormControl isInvalid={errors.length > 0}>
|
||||
<FormLabel>Select file to upload</FormLabel>
|
||||
<Input type='file' onChange={handleFile} accept='.json, .xlsx' />
|
||||
{errors.length === 0 ? (
|
||||
<FormHelperText>.XLSX .JSON with max 1MB</FormHelperText>
|
||||
) : (
|
||||
<FormErrorMessage className={style.flexColumnLeft}>
|
||||
{errors.map((error) => (
|
||||
<span key={error}>{error}</span>
|
||||
))}
|
||||
</FormErrorMessage>
|
||||
)}
|
||||
</FormControl>
|
||||
<div className={style.options}>
|
||||
<b>Options</b>
|
||||
<Checkbox ref={overrideOptionRef}>Import only events</Checkbox>
|
||||
<span className={style.notes}>This will prevent overriding user settings</span>
|
||||
</div>
|
||||
{file && (
|
||||
<div className={style.info}>
|
||||
<span>File ready to upload</span>
|
||||
<TooltipActionBtn
|
||||
clickHandler={() => setFile(null)}
|
||||
tooltip='Cancel'
|
||||
aria-label='Cancel'
|
||||
className={style.corner}
|
||||
size='sm'
|
||||
variant='ghosted'
|
||||
icon={<IoCloseSharp />}
|
||||
/>
|
||||
<ul className={style.infoList}>
|
||||
<li>{file.name}</li>
|
||||
<li>{`${(file.size / 1024).toFixed(2)}kb`}</li>
|
||||
<li>{file.type}</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<Progress value={progress} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
colorScheme='blue'
|
||||
disabled={!file || errors.length > 0}
|
||||
onClick={handleUpload}
|
||||
isLoading={progress < 0 && progress >= 100}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ type ValidationStatus = {
|
||||
};
|
||||
|
||||
export function validateFile(file: File): ValidationStatus {
|
||||
const status: ValidationStatus = { errors: [], isValid: true };
|
||||
const status:ValidationStatus = { errors: [], isValid: true };
|
||||
if (!file) {
|
||||
status.errors.push('No file to upload');
|
||||
status.isValid = false;
|
||||
@@ -1,47 +0,0 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Input, Select, Switch } from '@chakra-ui/react';
|
||||
|
||||
import { isStringBoolean } from '../../utils/viewUtils';
|
||||
|
||||
import { ParamField } from './types';
|
||||
|
||||
interface EditFormInputProps {
|
||||
paramField: ParamField;
|
||||
}
|
||||
|
||||
export default function ParamInput({ paramField }: EditFormInputProps) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { id, type } = paramField;
|
||||
|
||||
if (type === 'option') {
|
||||
const optionFromParams = searchParams.get(id);
|
||||
const defaultOptionValue = paramField.values.find((value) => value === optionFromParams);
|
||||
|
||||
return (
|
||||
<Select placeholder='Select an option' variant='ontime' name={id} defaultValue={defaultOptionValue}>
|
||||
{paramField.values.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'boolean') {
|
||||
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) ?? false;
|
||||
|
||||
// checked value should be 'true', so it can be captured by the form event
|
||||
return <Switch variant='ontime' name={id} defaultChecked={defaultCheckedValue} value='true' />;
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
const defaultNumberValue = searchParams.get(id) ?? '';
|
||||
|
||||
return <Input type='number' step='any' variant='ontime-filled' name={id} defaultValue={defaultNumberValue} />;
|
||||
}
|
||||
|
||||
const defaultStringValue = searchParams.get(id) ?? '';
|
||||
|
||||
return <Input variant='ontime-filled' name={id} defaultValue={defaultStringValue} />;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
@use '../../../theme/v2Styles' as *;
|
||||
@use '../../../theme/ontimeColours' as *;
|
||||
|
||||
.drawerContent {
|
||||
background-color: $gray-1200;
|
||||
}
|
||||
|
||||
.drawerHeader {
|
||||
@extend .drawerContent;
|
||||
color: $section-white;
|
||||
}
|
||||
|
||||
.drawerFooter {
|
||||
@extend .drawerContent;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
gap: $element-spacing;
|
||||
|
||||
button[type='reset'] {
|
||||
padding: 0 2em;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
button[type='submit'] {
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
}
|
||||
|
||||
.columnSection {
|
||||
display: flex;
|
||||
padding: $element-spacing;
|
||||
flex-direction: column;
|
||||
gap: $element-inner-spacing;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: $inner-section-text-size;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: $inner-section-text-size;
|
||||
display: block;
|
||||
color: $modal-note-color;
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { FormEvent, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import ParamInput from './ParamInput';
|
||||
import { ParamField } from './types';
|
||||
|
||||
import style from './ViewParamsEditor.module.scss';
|
||||
|
||||
interface EditFormDrawerProps {
|
||||
paramFields: ParamField[];
|
||||
}
|
||||
|
||||
export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
|
||||
useEffect(() => {
|
||||
const isEditing = searchParams.get('edit');
|
||||
|
||||
if (isEditing === 'true') {
|
||||
return onOpen();
|
||||
}
|
||||
}, [searchParams, onOpen]);
|
||||
|
||||
const onEditDrawerClose = () => {
|
||||
onClose();
|
||||
|
||||
searchParams.delete('edit');
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const clearParams = () => {
|
||||
setSearchParams();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||
formEvent.preventDefault();
|
||||
|
||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||
const newSearchParams = Object.entries(newParamsObject).reduce((newSearchParams, [id, value]) => {
|
||||
if (typeof value === 'string' && value.length) {
|
||||
newSearchParams.set(id, value);
|
||||
|
||||
return newSearchParams;
|
||||
}
|
||||
|
||||
return newSearchParams;
|
||||
}, new URLSearchParams());
|
||||
setSearchParams(newSearchParams);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer isOpen={isOpen} placement='right' onClose={onEditDrawerClose} size='lg'>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerHeader className={style.drawerHeader}>
|
||||
<DrawerCloseButton _hover={{ bg: '#ebedf0', color: '#333' }} size='lg' />
|
||||
Customise
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerBody className={style.drawerContent}>
|
||||
<form id='edit-params-form' onSubmit={onParamsFormSubmit}>
|
||||
{paramFields.map((field) => (
|
||||
<div key={field.title} className={style.columnSection}>
|
||||
<label className={style.label}>
|
||||
<span className={style.title}>{field.title}</span>
|
||||
<span className={style.description}>{field.description}</span>
|
||||
<ParamInput key={field.title} paramField={field} />
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</form>
|
||||
</DrawerBody>
|
||||
|
||||
<DrawerFooter className={style.drawerFooter}>
|
||||
<Button variant='ontime-ghosted' onClick={clearParams} type='reset'>
|
||||
Clear
|
||||
</Button>
|
||||
<Button variant='ontime-subtle' onClick={onEditDrawerClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
|
||||
Save
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import { ParamField } from './types';
|
||||
|
||||
export const TIME_FORMAT_OPTION: ParamField = {
|
||||
id: 'format',
|
||||
title: '12 / 24 hour timer',
|
||||
description: 'Whether to show the time in 12 or 24 hour mode. Overrides the global setting from preferences',
|
||||
type: 'option',
|
||||
values: ['12', '24'],
|
||||
};
|
||||
|
||||
export const CLOCK_OPTIONS: ParamField[] = [
|
||||
TIME_FORMAT_OPTION,
|
||||
{
|
||||
id: 'key',
|
||||
title: 'Key Colour',
|
||||
description: 'Background colour in hexadecimal',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
title: 'Text Colour',
|
||||
description: 'Text colour in hexadecimal',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'textbg',
|
||||
title: 'Text Background',
|
||||
description: 'Colour of text background in hexadecimal',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'font',
|
||||
title: 'Font',
|
||||
description: 'Font family, will use the fonts available in the system',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
title: 'Text Size',
|
||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: 'alignx',
|
||||
title: 'Align Horizontal',
|
||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: ['start', 'center', 'end'],
|
||||
},
|
||||
{
|
||||
id: 'offsetx',
|
||||
title: 'Offset Horizontal',
|
||||
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: 'aligny',
|
||||
title: 'Align Vertical',
|
||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: ['start', 'center', 'end'],
|
||||
},
|
||||
{
|
||||
id: 'offsety',
|
||||
title: 'Offset Vertical',
|
||||
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||
type: 'number',
|
||||
},
|
||||
];
|
||||
|
||||
export const TIMER_OPTIONS: ParamField[] = [TIME_FORMAT_OPTION];
|
||||
|
||||
export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||
{
|
||||
id: 'key',
|
||||
title: 'Key Colour',
|
||||
description: 'Background colour in hexadecimal',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
title: 'Text Colour',
|
||||
description: 'Text colour in hexadecimal',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'textbg',
|
||||
title: 'Text Background',
|
||||
description: 'Colour of text background in hexadecimal',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'font',
|
||||
title: 'Font',
|
||||
description: 'Font family, will use the fonts available in the system',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
title: 'Text Size',
|
||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: 'alignx',
|
||||
title: 'Align Horizontal',
|
||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: ['start', 'center', 'end'],
|
||||
},
|
||||
{
|
||||
id: 'offsetx',
|
||||
title: 'Offset Horizontal',
|
||||
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: 'aligny',
|
||||
title: 'Align Vertical',
|
||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: ['start', 'center', 'end'],
|
||||
},
|
||||
{
|
||||
id: 'offsety',
|
||||
title: 'Offset Vertical',
|
||||
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: 'hideovertime',
|
||||
title: 'Hide Overtime',
|
||||
description: 'Whether to supress overtime styles (red borders and red text)',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
id: 'hidemessages',
|
||||
title: 'Hide Message Overlay',
|
||||
description: 'Whether to hide the overlay from showing timer screen messages',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
id: 'hideendmessage',
|
||||
title: 'Hide End Message',
|
||||
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
|
||||
type: 'boolean',
|
||||
},
|
||||
];
|
||||
|
||||
export const LOWER_THIRDS_OPTIONS: ParamField[] = [
|
||||
{
|
||||
id: 'preset',
|
||||
title: 'Preset',
|
||||
description: 'Selects a style preset (0-1)',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
title: 'Size',
|
||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: 'transition',
|
||||
title: 'Transition',
|
||||
description: 'Transition in time in seconds (default 5)',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
title: 'Text Colour',
|
||||
description: 'Text colour in hexadecimal',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'bg',
|
||||
title: 'Text Background',
|
||||
description: 'Text background colour in hexadecimal',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
title: 'Key Colour',
|
||||
description: 'Screen background colour in hexadecimal',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
id: 'fadeout',
|
||||
title: 'Fadeout',
|
||||
description: 'Time (in seconds) the lower third displays before fading out',
|
||||
type: 'number',
|
||||
},
|
||||
];
|
||||
|
||||
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
|
||||
TIME_FORMAT_OPTION,
|
||||
{
|
||||
id: 'seconds',
|
||||
title: 'Show Seconds',
|
||||
description: 'Shows seconds in clock',
|
||||
type: 'boolean',
|
||||
},
|
||||
];
|
||||
@@ -1,12 +0,0 @@
|
||||
type BaseField = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type OptionsField = { type: 'option'; values: string[] };
|
||||
type StringField = { type: 'string' };
|
||||
type BooleanField = { type: 'boolean' };
|
||||
type NumberField = { type: 'number' };
|
||||
|
||||
export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField);
|
||||
@@ -1,9 +0,0 @@
|
||||
.contextMenuButton {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contextMenuBackdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// logic (with some modifications) culled from:
|
||||
// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx
|
||||
|
||||
import { createContext, ReactNode, useState } from 'react';
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { IconType } from '@react-icons/all-files';
|
||||
|
||||
import style from './ContextMenuContext.module.scss';
|
||||
|
||||
type ContextMenuCoords = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
type ContextMenuContextType = {
|
||||
createContextMenu: (options: Option[], menuCoordinates: ContextMenuCoords) => void;
|
||||
};
|
||||
|
||||
export const ContextMenuContext = createContext<ContextMenuContextType | null>(null);
|
||||
|
||||
export type Option = {
|
||||
label: string;
|
||||
icon: IconType;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
interface ContextMenuProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [coords, setCoords] = useState<ContextMenuCoords>({ x: 0, y: 0 });
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
|
||||
const onClose = () => {
|
||||
return setIsOpen(false);
|
||||
};
|
||||
|
||||
const createContextMenu = (options: Option[], menuCoords: ContextMenuCoords) => {
|
||||
setCoords(menuCoords);
|
||||
setOptions(options);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuContext.Provider value={{ createContextMenu }}>
|
||||
{children}
|
||||
{isOpen && (
|
||||
<>
|
||||
<div className={style.contextMenuBackdrop} />
|
||||
<Menu isOpen gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
|
||||
<MenuButton
|
||||
className={style.contextMenuButton}
|
||||
aria-hidden
|
||||
w={1}
|
||||
h={1}
|
||||
style={{
|
||||
left: coords.x,
|
||||
top: coords.y,
|
||||
}}
|
||||
/>
|
||||
<MenuList>
|
||||
{options.map(({ label, icon: Icon, onClick }, i) => (
|
||||
<MenuItem key={i} icon={<Icon />} onClick={onClick}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</ContextMenuContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { ALIASES } from '../api/apiConstants';
|
||||
import { getAliases } from '../api/ontimeApi';
|
||||
|
||||
export default function useAliases() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
const { data, status, isError, refetch } = useQuery({
|
||||
queryKey: ALIASES,
|
||||
queryFn: getAliases,
|
||||
placeholderData: [],
|
||||
@@ -15,5 +15,5 @@ export default function useAliases() {
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data, status, isFetching, isError, refetch };
|
||||
}
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { fetchEventData } from '../api/eventDataApi';
|
||||
import { eventDataPlaceholder } from '../models/EventData';
|
||||
|
||||
export default function useEventData() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
const { data, status, isError, refetch } = useQuery({
|
||||
queryKey: EVENT_DATA,
|
||||
queryFn: fetchEventData,
|
||||
placeholderData: eventDataPlaceholder,
|
||||
@@ -16,5 +16,5 @@ export default function useEventData() {
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data, status, isFetching, isError, refetch };
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { OSCSettings } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { OSC_SETTINGS } from '../api/apiConstants';
|
||||
import { getOSC, postOSC, postOscSubscriptions } from '../api/ontimeApi';
|
||||
import { getOSC, postOSC } from '../api/ontimeApi';
|
||||
import { oscPlaceholderSettings } from '../models/OscSettings';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
export default function useOscSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
const { data, status, isError, refetch } = useQuery({
|
||||
queryKey: OSC_SETTINGS,
|
||||
queryFn: getOSC,
|
||||
placeholderData: oscPlaceholderSettings,
|
||||
@@ -18,8 +18,7 @@ export default function useOscSettings() {
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
// we need to jump through some hoops because of the type op port
|
||||
return { data: data! as unknown as OSCSettings, status, isFetching, isError, refetch };
|
||||
return { data: data! as unknown as OSCSettings, status, isError, refetch };
|
||||
}
|
||||
|
||||
export function useOscSettingsMutation() {
|
||||
@@ -30,11 +29,3 @@ export function useOscSettingsMutation() {
|
||||
});
|
||||
return { isLoading, mutateAsync };
|
||||
}
|
||||
|
||||
export function usePostOscSubscriptions() {
|
||||
const { isLoading, mutateAsync } = useMutation({
|
||||
mutationFn: postOscSubscriptions,
|
||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
||||
});
|
||||
return { isLoading, mutateAsync };
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getSettings } from '../api/ontimeApi';
|
||||
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
||||
|
||||
export default function useSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
const { data, status, isError, refetch } = useQuery({
|
||||
queryKey: APP_SETTINGS,
|
||||
queryFn: getSettings,
|
||||
placeholderData: ontimePlaceholderSettings,
|
||||
@@ -16,5 +16,5 @@ export default function useSettings() {
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data, status, isFetching, isError, refetch };
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getUserFields } from '../api/ontimeApi';
|
||||
import { userFieldsPlaceholder } from '../models/UserFields';
|
||||
|
||||
export default function useUserFields() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
const { data, status, isError, refetch } = useQuery({
|
||||
queryKey: USERFIELDS,
|
||||
queryFn: getUserFields,
|
||||
placeholderData: userFieldsPlaceholder,
|
||||
@@ -16,5 +16,5 @@ export default function useUserFields() {
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data, status, isFetching, isError, refetch };
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getView } from '../api/ontimeApi';
|
||||
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
||||
|
||||
export default function useViewSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
const { data, status, isError, refetch } = useQuery({
|
||||
queryKey: VIEW_SETTINGS,
|
||||
queryFn: getView,
|
||||
placeholderData: viewsSettingsPlaceholder,
|
||||
@@ -16,5 +16,5 @@ export default function useViewSettings() {
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data, status, isError, refetch, isFetching };
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { MouseEvent, useContext } from 'react';
|
||||
|
||||
import { ContextMenuContext, Option } from '../context/ContextMenuContext';
|
||||
|
||||
export const useContextMenu = <T extends HTMLElement>(options: Option[]) => {
|
||||
const contextMenuContext = useContext(ContextMenuContext);
|
||||
|
||||
if (contextMenuContext === null) {
|
||||
throw new Error('useContextMenu should be wrapped by ContextMenuProvider');
|
||||
}
|
||||
|
||||
const { createContextMenu } = contextMenuContext;
|
||||
|
||||
const localCreateContextMenu = (contextMenuEvent: MouseEvent<T, globalThis.MouseEvent>) => {
|
||||
// prevent browser default context menu from showing up
|
||||
contextMenuEvent.preventDefault();
|
||||
|
||||
const { pageX, pageY } = contextMenuEvent;
|
||||
return createContextMenu(options, { x: pageX, y: pageY });
|
||||
};
|
||||
|
||||
return [localCreateContextMenu];
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
|
||||
export default function useFullscreen() {
|
||||
const [isFullScreen, setFullScreen] = useState(document.fullscreenElement);
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
setFullScreen(document.fullscreenElement);
|
||||
};
|
||||
document.addEventListener('fullscreenchange', handleChange, { passive: true });
|
||||
document.addEventListener('resize', handleChange, { passive: true });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('fullscreenchange', handleChange, { passive: true });
|
||||
document.removeEventListener('resize', handleChange, { passive: true });
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
if (!document.fullscreenElement && !document.webkitIsFullScreen) {
|
||||
// Fullscreen mode is not active, so we can enter fullscreen mode
|
||||
if (document.documentElement.requestFullscreen) {
|
||||
// Standard fullscreen API is supported
|
||||
document.documentElement.requestFullscreen();
|
||||
} else if (document.documentElement.webkitRequestFullscreen) {
|
||||
// iOS Safari fullscreen API is supported
|
||||
document.documentElement.webkitRequestFullscreen();
|
||||
}
|
||||
} else {
|
||||
// Fullscreen mode is active, so we can exit fullscreen mode
|
||||
if (document.exitFullscreen) {
|
||||
// Standard fullscreen API is supported
|
||||
document.exitFullscreen();
|
||||
} else if (document.webkitCancelFullscreen) {
|
||||
// iOS Safari fullscreen API is supported
|
||||
document.webkitCancelFullscreen();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { isFullScreen, toggleFullScreen };
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
interface WebkitDocument extends Document {
|
||||
webkitFullscreenElement?: Element | null;
|
||||
webkitIsFullScreen?: boolean;
|
||||
webkitExitFullscreen?: () => Promise<void>;
|
||||
webkitRequestFullscreen?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function useFullscreen() {
|
||||
const [isFullScreen, setFullScreen] = useState(
|
||||
document.fullscreenElement || (document as WebkitDocument).webkitFullscreenElement,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
if (typeof (document as WebkitDocument).webkitFullscreenElement !== 'undefined') {
|
||||
setFullScreen((document as WebkitDocument).webkitFullscreenElement);
|
||||
} else {
|
||||
setFullScreen(document.fullscreenElement);
|
||||
}
|
||||
};
|
||||
(document as WebkitDocument).addEventListener('webkitfullscreenchange', handleChange, { passive: true });
|
||||
document.addEventListener('fullscreenchange', handleChange, { passive: true });
|
||||
document.addEventListener('resize', handleChange, { passive: true });
|
||||
|
||||
return () => {
|
||||
(document as WebkitDocument).removeEventListener('webkitfullscreenchange', handleChange);
|
||||
document.removeEventListener('fullscreenchange', handleChange);
|
||||
document.removeEventListener('resize', handleChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
if (!document.fullscreenElement && !(document as WebkitDocument).webkitIsFullScreen) {
|
||||
// Fullscreen mode is not active, so we can enter fullscreen mode
|
||||
if (document.documentElement.requestFullscreen) {
|
||||
// Standard fullscreen API is supported
|
||||
document.documentElement.requestFullscreen();
|
||||
// @ts-expect-error -- the whole casting dance is not worth it
|
||||
} else if (document.documentElement.webkitRequestFullscreen) {
|
||||
// iOS Safari fullscreen API is supported
|
||||
// @ts-expect-error -- we know this is not undefined now
|
||||
(document.documentElement as WebkitDocument).webkitRequestFullscreen();
|
||||
}
|
||||
} else {
|
||||
// Fullscreen mode is active, so we can exit fullscreen mode
|
||||
if (document.exitFullscreen) {
|
||||
// Standard fullscreen API is supported
|
||||
document.exitFullscreen();
|
||||
} else if ((document as WebkitDocument).webkitExitFullscreen) {
|
||||
// iOS Safari fullscreen API is supported
|
||||
// @ts-expect-error -- we know this is not undefined now
|
||||
(document as WebkitDocument).webkitExitFullscreen();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { isFullScreen, toggleFullScreen };
|
||||
}
|
||||
@@ -1,25 +1,18 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type UseKeyDown = (callback: () => void, targetKey: string, options?: { isDisabled?: boolean }) => void;
|
||||
|
||||
export const useKeyDown: UseKeyDown = (callback, targetKey, options = {}) => {
|
||||
const { isDisabled = false } = options;
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const targetKeyPressed = event.key === targetKey && !event.repeat;
|
||||
if (targetKeyPressed && !isDisabled) {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
}
|
||||
},
|
||||
[callback, isDisabled, targetKey],
|
||||
);
|
||||
export const useKeyDown = (callback: () => void, targetKey: string) => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const targetKeyPressed = event.key === targetKey && !event.repeat;
|
||||
if (targetKeyPressed) {
|
||||
event.preventDefault();
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [onKeyDown]);
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -32,8 +32,6 @@ export const setMessage = {
|
||||
lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload),
|
||||
lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload),
|
||||
onAir: (payload: boolean) => socketSendJson('set-onAir', payload),
|
||||
timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', payload),
|
||||
timerBlackout: (payload: boolean) => socketSendJson('set-timer-blackout', payload),
|
||||
};
|
||||
|
||||
export const usePlaybackControl = () => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Alias } from 'ontime-types';
|
||||
|
||||
export const aliasPlaceholder: Alias = {
|
||||
enabled: false,
|
||||
alias: '',
|
||||
pathAndParams: '',
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export const ontimeVars = [
|
||||
{
|
||||
name: '$timer',
|
||||
description: 'Current running timer',
|
||||
},
|
||||
{
|
||||
name: '$title',
|
||||
description: 'Current title',
|
||||
},
|
||||
{
|
||||
name: '$presenter',
|
||||
description: 'Current timer',
|
||||
},
|
||||
{
|
||||
name: '$subtitle',
|
||||
description: 'Current subtitle',
|
||||
},
|
||||
{
|
||||
name: '$next-title',
|
||||
description: 'Next title',
|
||||
},
|
||||
{
|
||||
name: '$next-presenter',
|
||||
description: 'Next timer',
|
||||
},
|
||||
{
|
||||
name: '$next-subtitle',
|
||||
description: 'Next subtitle',
|
||||
},
|
||||
];
|
||||
@@ -22,8 +22,6 @@ export const runtimeStorePlaceholder = {
|
||||
timerMessage: {
|
||||
text: '',
|
||||
visible: false,
|
||||
timerBlink: false,
|
||||
timerBlackout: false,
|
||||
},
|
||||
publicMessage: {
|
||||
text: '',
|
||||
|
||||
@@ -1,4 +1,100 @@
|
||||
import { forgivingStringToMillis, millisToDelayString, millisToMinutes, millisToSeconds } from '../dateConfig';
|
||||
import {
|
||||
forgivingStringToMillis,
|
||||
formatDisplay,
|
||||
isTimeString,
|
||||
millisToDelayString,
|
||||
millisToMinutes,
|
||||
millisToSeconds,
|
||||
} from '../dateConfig';
|
||||
|
||||
describe('test string from formatDisplay function', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with not numbers', () => {
|
||||
const t = { val: 'test', result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86400 (24 hours)', () => {
|
||||
const t = { val: 86400000, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: '00:00:01' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: '00:00:01' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test string from formatDisplay function with hidezero', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86400 (24 hours)', () => {
|
||||
const t = { val: 86400000, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: '00:01' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: '00:01' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test millisToSeconds function', () => {
|
||||
it('test with null values', () => {
|
||||
@@ -74,6 +170,31 @@ describe('test millisToMinutes function', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('test isTimeString() function', () => {
|
||||
it('it validates time strings', () => {
|
||||
const ts = ['2', '2:10', '2:10:22'];
|
||||
for (const s of ts) {
|
||||
expect(isTimeString(s)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('it fails overloaded times', () => {
|
||||
const ts = ['70', '89:10', '26:10:22'];
|
||||
for (const s of ts) {
|
||||
expect(isTimeString(s)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('test isTimeString() function handle different separators', () => {
|
||||
const ts = ['2:10', '2,10', '2.10'];
|
||||
for (const s of ts) {
|
||||
it(`it handles ${s}`, () => {
|
||||
expect(isTimeString(s)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('test forgivingStringToMillis()', () => {
|
||||
describe('function handles time with no separators', () => {
|
||||
const testData = [
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// we need to this as a promise because safari
|
||||
export default async function copyToClipboard(text: string) {
|
||||
setTimeout(async () => await navigator.clipboard.writeText(text));
|
||||
}
|
||||
@@ -5,6 +5,29 @@ import { mth, mtm, mts } from './timeConstants';
|
||||
export const timeFormat = 'HH:mm';
|
||||
export const timeFormatSeconds = 'HH:mm:ss';
|
||||
|
||||
/**
|
||||
* another go at simpler string formatting (counters)
|
||||
* @description Converts seconds to string representing time
|
||||
* @param {number | null} milliseconds - time in seconds
|
||||
* @param {boolean} [hideZero] - whether to show hours in case its 00
|
||||
* @returns {string} String representing absolute time 00:12:02
|
||||
*/
|
||||
export function formatDisplay(milliseconds: number | null, hideZero = false): string {
|
||||
if (typeof milliseconds !== 'number') {
|
||||
return hideZero ? '00:00' : '00:00:00';
|
||||
}
|
||||
|
||||
// add an extra 0 if necessary
|
||||
const format = (val: number) => `0${Math.floor(val)}`.slice(-2);
|
||||
|
||||
const s = Math.abs(millisToSeconds(milliseconds));
|
||||
const hours = Math.floor((s / 3600) % 24);
|
||||
const minutes = Math.floor((s % 3600) / 60);
|
||||
|
||||
if (hideZero && hours < 1) return [minutes, s % 60].map(format).join(':');
|
||||
return [hours, minutes, s % 60].map(format).join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Converts milliseconds to seconds
|
||||
* @param {number | null} millis - time in seconds
|
||||
@@ -26,6 +49,26 @@ export const millisToMinutes = (millis: number): number => {
|
||||
return millis < 0 ? Math.ceil(millis / mtm) : Math.floor(millis / mtm);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Validates a time string
|
||||
* @param {string} string - time string "23:00:12"
|
||||
* @returns {boolean} string represents time
|
||||
*/
|
||||
export const isTimeString = (string: string): boolean => {
|
||||
// ^ # Start of string
|
||||
// (?: # Try to match...
|
||||
// (?: # Try to match...
|
||||
// ([01]?\d|2[0-3]): # HH:
|
||||
// )? # (optionally).
|
||||
// ([0-5]?\d): # MM: (required)
|
||||
// )? # (entire group optional, so either HH:MM:, MM: or nothing)
|
||||
// ([0-5]?\d) # SS (required)
|
||||
// $ # End of string
|
||||
|
||||
const regex = /^(?:(?:([01]?\d|2[0-3])[:,.])?([0-5]?\d)[:,.])?([0-5]?\d)$/;
|
||||
return regex.test(string);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description safe parse string to int
|
||||
* @param {string} valueAsString
|
||||
|
||||
@@ -8,16 +8,13 @@ import { runtime } from '../stores/runtime';
|
||||
export let websocket: WebSocket | null = null;
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
const reconnectInterval = 1000;
|
||||
export let shouldReconnect = true;
|
||||
export let hasConnected = false;
|
||||
export let reconnectAttempts = 0;
|
||||
let shouldReconnect = true;
|
||||
|
||||
export const connectSocket = () => {
|
||||
websocket = new WebSocket(websocketUrl);
|
||||
|
||||
websocket.onopen = () => {
|
||||
clearTimeout(reconnectTimeout as NodeJS.Timeout);
|
||||
hasConnected = true;
|
||||
reconnectAttempts = 0;
|
||||
};
|
||||
|
||||
websocket.onclose = () => {
|
||||
@@ -26,7 +23,6 @@ export const connectSocket = () => {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
console.warn('WebSocket: attempting reconnect');
|
||||
if (websocket && websocket.readyState === WebSocket.CLOSED) {
|
||||
reconnectAttempts += 1;
|
||||
connectSocket();
|
||||
}
|
||||
}, reconnectInterval);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { IoEye } from '@react-icons/all-files/io5/IoEye';
|
||||
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
|
||||
|
||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import style from './InputRow.module.scss';
|
||||
@@ -15,19 +14,17 @@ interface InputRowProps {
|
||||
visible?: boolean;
|
||||
actionHandler: (action: string, payload: object) => void;
|
||||
changeHandler: (newValue: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function InputRow(props: InputRowProps) {
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler, className } = props;
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
|
||||
|
||||
const handleInputChange = (newValue: string) => {
|
||||
changeHandler(newValue);
|
||||
};
|
||||
const classes = cx([style.inputRow, className]);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={style.inputRow}>
|
||||
<label className={`${style.label} ${visible ? style.active : ''}`}>{label}</label>
|
||||
<div className={style.inputItems}>
|
||||
<Input
|
||||
@@ -42,7 +39,7 @@ export default function InputRow(props: InputRowProps) {
|
||||
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
||||
aria-label={`Toggle ${label}`}
|
||||
openDelay={tooltipDelayMid}
|
||||
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||
icon={visible? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||
size='sm'
|
||||
/>
|
||||
|
||||
@@ -7,18 +7,12 @@
|
||||
}
|
||||
|
||||
.onAirSection {
|
||||
margin-top: $section-spacing;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $element-spacing;
|
||||
}
|
||||
|
||||
.buttonSection {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $element-spacing;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
@@ -27,7 +21,3 @@
|
||||
color: $action-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.padTop {
|
||||
margin-top: $section-spacing;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoEye } from '@react-icons/all-files/io5/IoEye';
|
||||
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
|
||||
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
|
||||
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
|
||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||
import { IoSunnyOutline } from '@react-icons/all-files/io5/IoSunnyOutline';
|
||||
|
||||
import { setMessage, useMessageControl } from '../../../common/hooks/useSocket';
|
||||
|
||||
@@ -17,6 +13,14 @@ export default function MessageControl() {
|
||||
|
||||
return (
|
||||
<div className={style.messageContainer}>
|
||||
<InputRow
|
||||
label='Timer screen message'
|
||||
placeholder='Shown in stage timer'
|
||||
text={data.timerMessage.text || ''}
|
||||
visible={data.timerMessage.visible || false}
|
||||
changeHandler={(newValue) => setMessage.presenterText(newValue)}
|
||||
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
|
||||
/>
|
||||
<InputRow
|
||||
label='Public / Backstage screen message'
|
||||
placeholder='Shown in public and backstage screens'
|
||||
@@ -33,33 +37,7 @@ export default function MessageControl() {
|
||||
changeHandler={(newValue) => setMessage.lowerText(newValue)}
|
||||
actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)}
|
||||
/>
|
||||
<InputRow
|
||||
label='Timer message'
|
||||
placeholder='Shown in stage timer'
|
||||
text={data.timerMessage.text || ''}
|
||||
visible={data.timerMessage.visible || false}
|
||||
changeHandler={(newValue) => setMessage.presenterText(newValue)}
|
||||
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
|
||||
/>
|
||||
<div className={style.buttonSection}>
|
||||
<label className={style.label}>Timer messsage blink</label>
|
||||
<label className={style.label}>Blackout timer screens</label>
|
||||
<Button
|
||||
className={`${data.timerMessage.timerBlink ? style.blink : ''}`}
|
||||
variant={data.timerMessage.timerBlink ? 'ontime-filled' : 'ontime-subtle'}
|
||||
leftIcon={data.timerMessage.timerBlink ? <IoSunny size='24px' /> : <IoSunnyOutline size='24px' />}
|
||||
onClick={() => setMessage.timerBlink(!data.timerMessage.timerBlink)}
|
||||
data-testid='toggle timer blink'
|
||||
/>
|
||||
<Button
|
||||
className={style.blackoutButton}
|
||||
variant={data.timerMessage.timerBlackout ? 'ontime-filled' : 'ontime-subtle'}
|
||||
leftIcon={data.timerMessage.timerBlackout ? <IoEye size='24px' /> : <IoEyeOffOutline size='24px' />}
|
||||
onClick={() => setMessage.timerBlackout(!data.timerMessage.timerBlackout)}
|
||||
data-testid='toggle timer blackout'
|
||||
/>
|
||||
</div>
|
||||
<div className={`${style.onAirSection} ${style.padTop}`}>
|
||||
<div className={style.onAirSection}>
|
||||
<label className={style.label}>Toggle On Air state</label>
|
||||
<Button
|
||||
variant={data.onAir ? 'ontime-filled' : 'ontime-subtle'}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
|
||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||
@@ -10,14 +11,14 @@ import style from '../../editors/Editor.module.scss';
|
||||
|
||||
const MessageControlExport = () => {
|
||||
return (
|
||||
<div className={style.messages} data-testid='panel-messages-control'>
|
||||
<Box className={style.messages} data-testid='panel-messages-control'>
|
||||
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} />
|
||||
<div className={style.content}>
|
||||
<ErrorBoundary>
|
||||
<MessageControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
|
||||
import ErrorBoundary from '../../../common/components/error-boundary/ErrorBoundary';
|
||||
@@ -10,14 +11,14 @@ import style from '../../editors/Editor.module.scss';
|
||||
|
||||
const TimerControlExport = () => {
|
||||
return (
|
||||
<div className={style.playback} data-testid='panel-timer-control'>
|
||||
<Box className={style.playback} data-testid='panel-timer-control'>
|
||||
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} />
|
||||
<div className={style.content}>
|
||||
<ErrorBoundary>
|
||||
<PlaybackControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -7,13 +7,13 @@
|
||||
"go playback"
|
||||
"go transport"
|
||||
"extra extra";
|
||||
grid-template-rows: repeat(3, 2rem);
|
||||
grid-template-rows: repeat(3, 32px);
|
||||
gap: $element-spacing;
|
||||
}
|
||||
|
||||
.go {
|
||||
grid-area: go;
|
||||
font-size: 5rem;
|
||||
font-size: 5em;
|
||||
}
|
||||
|
||||
@mixin spaced-flex {
|
||||
|
||||
@@ -5,14 +5,14 @@ import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
|
||||
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
|
||||
import { IoReload } from '@react-icons/all-files/io5/IoReload';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { IoTime } from '@react-icons/all-files/io5/IoTime';
|
||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||
import { Playback } from 'ontime-types';
|
||||
import { validatePlayback } from 'ontime-utils';
|
||||
|
||||
import { setPlayback } from '../../../../common/hooks/useSocket';
|
||||
import { tooltipDelayMid } from '../../../../ontimeConfig';
|
||||
import TapButton from '../tap-button/TapButton';
|
||||
|
||||
import styles from './PlaybackButtons.module.scss';
|
||||
import style from './PlaybackButtons.module.scss';
|
||||
|
||||
interface PlaybackButtonsProps {
|
||||
@@ -35,13 +35,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
||||
const noEvents = numEvents === 0;
|
||||
|
||||
const disableGo = isRolling || noEvents || (isLast && !isArmed);
|
||||
const disablePrev = noEvents || isFirst;
|
||||
|
||||
const playbackCan = validatePlayback(playback);
|
||||
const disableStart = !playbackCan.start;
|
||||
const disablePause = !playbackCan.pause;
|
||||
const disableRoll = !playbackCan.roll || noEvents;
|
||||
const disableStop = !playbackCan.stop;
|
||||
const disablePrev = isRolling || noEvents || isFirst;
|
||||
|
||||
const goModeText = selectedEventIndex === null || isArmed ? 'Start' : 'Next';
|
||||
const goModeAction = () => {
|
||||
@@ -53,16 +47,26 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.buttonContainer}>
|
||||
<TapButton disabled={disableGo} onClick={goModeAction} aspect='fill' className={style.go}>
|
||||
<div className={styles.buttonContainer}>
|
||||
<TapButton disabled={disableGo} onClick={goModeAction} aspect='fill' className={styles.go}>
|
||||
{goModeText}
|
||||
</TapButton>
|
||||
<div className={style.playbackContainer}>
|
||||
<TapButton onClick={setPlayback.start} disabled={disableStart} theme={Playback.Play} active={isPlaying}>
|
||||
<TapButton
|
||||
onClick={setPlayback.start}
|
||||
disabled={isStopped || isRolling}
|
||||
theme={Playback.Play}
|
||||
active={isPlaying}
|
||||
>
|
||||
<IoPlay />
|
||||
</TapButton>
|
||||
|
||||
<TapButton onClick={setPlayback.pause} disabled={disablePause} theme={Playback.Pause} active={isPaused}>
|
||||
<TapButton
|
||||
onClick={setPlayback.pause}
|
||||
disabled={isStopped || isRolling || isArmed}
|
||||
theme={Playback.Pause}
|
||||
active={isPaused}
|
||||
>
|
||||
<IoPause />
|
||||
</TapButton>
|
||||
</div>
|
||||
@@ -78,17 +82,22 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={style.extra}>
|
||||
<TapButton onClick={setPlayback.roll} disabled={disableRoll} theme={Playback.Roll} active={isRolling}>
|
||||
<IoTime />
|
||||
<div className={styles.extra}>
|
||||
<TapButton
|
||||
onClick={setPlayback.roll}
|
||||
disabled={!isStopped || noEvents}
|
||||
theme={Playback.Roll}
|
||||
active={isRolling}
|
||||
>
|
||||
<IoTimeOutline />
|
||||
</TapButton>
|
||||
<Tooltip label='Reload event' openDelay={tooltipDelayMid}>
|
||||
<TapButton onClick={setPlayback.reload} disabled={isStopped}>
|
||||
<TapButton onClick={setPlayback.reload} disabled={isStopped || isRolling}>
|
||||
<IoReload className={style.invertX} />
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
|
||||
<TapButton onClick={setPlayback.stop} disabled={disableStop} theme={Playback.Stop}>
|
||||
<TapButton onClick={setPlayback.stop} disabled={isStopped && !isRolling} theme={Playback.Stop}>
|
||||
<IoStop />
|
||||
</TapButton>
|
||||
</Tooltip>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
'ind clk clk btn'
|
||||
'... sta fin btn';
|
||||
grid-template-rows: 1fr auto;
|
||||
grid-template-columns: 1.25rem 1fr 1fr 5rem;
|
||||
grid-template-columns: 1.5em 1fr 1fr 5em;
|
||||
gap: $element-inner-spacing;
|
||||
justify-items: start;
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
.timer {
|
||||
grid-area: clk;
|
||||
white-space: nowrap;
|
||||
max-width: 18.75rem;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.indicators {
|
||||
@@ -51,7 +51,7 @@
|
||||
.indNegativeActive {
|
||||
margin: 0 auto;
|
||||
width: 90%;
|
||||
height: 0.25rem;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.indNegativeActive {
|
||||
@@ -80,7 +80,7 @@
|
||||
.start,
|
||||
.finish,
|
||||
.roll {
|
||||
height: 1.5rem;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.start {
|
||||
|
||||
@@ -6,7 +6,7 @@ $button-color-white: $gray-50;
|
||||
|
||||
@mixin tap-factory($theme-color) {
|
||||
font-family: $ontime-font-family;
|
||||
font-size: calc(1rem + 2px);
|
||||
font-size: 18px;
|
||||
border-radius: $component-border-radius-md;
|
||||
width: 100%;
|
||||
transition-property: color, background-color;
|
||||
@@ -77,13 +77,17 @@ $button-color-white: $gray-50;
|
||||
@include tap-factory($ontime-stop);
|
||||
}
|
||||
|
||||
.tapButton.normal {
|
||||
aspect-ratio: 3/1;
|
||||
}
|
||||
|
||||
.tapButton.square {
|
||||
aspect-ratio: 1;
|
||||
font-size: calc(1rem - 2px);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tapButton.fill {
|
||||
aspect-ratio: unset;
|
||||
height: 100%;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@use '../../theme/ontimeColours' as *;
|
||||
@use '../../theme/v2Styles' as *;
|
||||
|
||||
$menu-width: 2.75rem;
|
||||
$rundown-width: 44.5rem;
|
||||
$playback-width: 26rem;
|
||||
$menu-width: 48px;
|
||||
$rundown-width: 46em;
|
||||
$playback-width: 450px;
|
||||
|
||||
@mixin absolute-top-right($distance) {
|
||||
position: absolute;
|
||||
@@ -20,7 +20,7 @@ $playback-width: 26rem;
|
||||
|
||||
.corner {
|
||||
display: none;
|
||||
@include absolute-top-right(0.5rem);
|
||||
@include absolute-top-right(8px);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ $playback-width: 26rem;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
color: $ui-white;
|
||||
padding: 1rem 0.5rem;
|
||||
padding: 16px 8px;
|
||||
font-family: $ontime-font-family;
|
||||
|
||||
display: grid;
|
||||
@@ -39,7 +39,7 @@ $playback-width: 26rem;
|
||||
grid-template-areas:
|
||||
'sett rundown play info'
|
||||
'sett rundown mess info';
|
||||
gap: 0.5rem;
|
||||
gap: 8px;
|
||||
|
||||
.rundown,
|
||||
.playback,
|
||||
@@ -59,7 +59,7 @@ $playback-width: 26rem;
|
||||
.mainContainer {
|
||||
height: 100%;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: $menu-width 1fr $playback-width;
|
||||
grid-template-columns: $menu-width $rundown-width auto;
|
||||
|
||||
.info {
|
||||
visibility: hidden;
|
||||
@@ -74,7 +74,7 @@ $playback-width: 26rem;
|
||||
grid-template-rows: 100%;
|
||||
grid-template-columns: $menu-width $rundown-width;
|
||||
grid-template-areas:
|
||||
'sett rundown';
|
||||
'sett even';
|
||||
|
||||
.info,
|
||||
.messages,
|
||||
@@ -135,7 +135,7 @@ $playback-width: 26rem;
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
background-color: $bg-container-l2;
|
||||
padding: 1rem;
|
||||
padding: 0.8em 1.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -165,7 +165,7 @@ $playback-width: 26rem;
|
||||
|
||||
.header {
|
||||
background-color: $gray-1200;
|
||||
padding: 0.5rem;
|
||||
padding: 8px;
|
||||
border-left: 1px solid $white-10;
|
||||
border-radius: 0 8px 0 0;
|
||||
}
|
||||
@@ -176,14 +176,14 @@ $playback-width: 26rem;
|
||||
height: 100%;
|
||||
|
||||
.content {
|
||||
height: calc(100% - 1.5rem);
|
||||
height: calc(100% - 24px);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
grid-area: info;
|
||||
min-width: 17rem;
|
||||
min-width: 17em;
|
||||
max-width: 800px;
|
||||
|
||||
.content {
|
||||
@@ -195,20 +195,20 @@ $playback-width: 26rem;
|
||||
|
||||
.messages {
|
||||
grid-area: mess;
|
||||
min-width: 24rem;
|
||||
}
|
||||
|
||||
.playback {
|
||||
grid-area: play;
|
||||
min-height: 250px;
|
||||
max-height: 380px;
|
||||
min-width: 26rem;
|
||||
min-width: 450px;
|
||||
}
|
||||
|
||||
.mainContainer > .settings {
|
||||
grid-area: sett;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0 0.5rem 0 0;
|
||||
padding: 0 8px 0 0;
|
||||
width: fit-content;
|
||||
|
||||
display: flex;
|
||||
@@ -216,11 +216,5 @@ $playback-width: 26rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.rundown {
|
||||
.content {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { lazy, useEffect } from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { Box, useDisclosure } from '@chakra-ui/react';
|
||||
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import UploadModal from '../../common/components/upload-modal/UploadModal';
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
import AboutModal from '../modals/about-modal/AboutModal';
|
||||
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
|
||||
import QuickStart from '../modals/quick-start/QuickStart';
|
||||
import UploadModal from '../modals/upload-modal/UploadModal';
|
||||
import SettingsModal from '../modals/settings-modal/SettingsModal';
|
||||
|
||||
import styles from './Editor.module.scss';
|
||||
|
||||
const Rundown = lazy(() => import('../rundown/RundownExport'));
|
||||
const TimerControl = lazy(() => import('../control/playback/TimerControlExport'));
|
||||
const MessageControl = lazy(() => import('../control/message/MessageControlExport'));
|
||||
const Info = lazy(() => import('../info/InfoExport'));
|
||||
const EventEditor = lazy(() => import('../event-editor/EventEditorExport'));
|
||||
|
||||
const IntegrationModal = lazy(() => import('../modals/integration-modal/IntegrationModal'));
|
||||
const SettingsModal = lazy(() => import('../modals/settings-modal/SettingsModal'));
|
||||
const Rundown = lazy(() => import('../../features/rundown/RundownExport'));
|
||||
const TimerControl = lazy(() => import('../../features/control/playback/TimerControlExport'));
|
||||
const MessageControl = lazy(() => import('../../features/control/message/MessageControlExport'));
|
||||
const Info = lazy(() => import('../../features/info/InfoExport'));
|
||||
const EventEditor = lazy(() => import('../../features/event-editor/EventEditorExport'));
|
||||
|
||||
export default function Editor() {
|
||||
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
|
||||
@@ -44,7 +43,7 @@ export default function Editor() {
|
||||
<SettingsModal isOpen={isSettingsOpen} onClose={onSettingsClose} />
|
||||
</ErrorBoundary>
|
||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||
<div id='settings' className={styles.settings}>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar
|
||||
onSettingsOpen={onSettingsOpen}
|
||||
@@ -60,7 +59,7 @@ export default function Editor() {
|
||||
onQuickStartOpen={onQuickStartOpen}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
<Rundown />
|
||||
<MessageControl />
|
||||
<TimerControl />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@use '../../theme/v2Styles' as *;
|
||||
|
||||
.eventEditor {
|
||||
padding: 1rem 2rem 2rem 2rem;
|
||||
padding: 16px 32px 32px 32px;
|
||||
width: 100%;
|
||||
gap: max(1rem, 2vh);
|
||||
gap: max(16px, 2vh);
|
||||
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
@@ -15,17 +15,15 @@
|
||||
.timeSettings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.eventInfo {
|
||||
grid-area: eventInfo;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.eventId {
|
||||
margin-left:$element-spacing;
|
||||
.eventId {
|
||||
margin-left: $element-inner-spacing;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +35,7 @@
|
||||
.timeOptions {
|
||||
grid-area: timeOptions;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.titles {
|
||||
@@ -55,19 +53,19 @@
|
||||
|
||||
.left {
|
||||
grid-area: left;
|
||||
padding: 0 1rem;
|
||||
padding: 0 16px;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
|
||||
.right {
|
||||
padding-left: 1rem;
|
||||
padding-left: 16px;
|
||||
grid-area: right;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin input-label() {
|
||||
font-size: calc(1rem - 3px);
|
||||
font-size: 13px;
|
||||
color: $label-gray;
|
||||
}
|
||||
|
||||
@@ -86,32 +84,32 @@
|
||||
}
|
||||
|
||||
&.publicToggle {
|
||||
height: 2rem;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 1.25rem;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.padTop {
|
||||
margin-top: 0.5rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.fullHeight {
|
||||
|
||||
@@ -42,9 +42,7 @@ export default function EventEditor() {
|
||||
<div className={style.eventEditor}>
|
||||
<div className={style.eventInfo}>
|
||||
Event ID
|
||||
<span className={style.eventId}>
|
||||
<CopyTag label={event.id}>{event.id}</CopyTag>
|
||||
</span>
|
||||
<span className={style.eventId}>{event.id}</span>
|
||||
</div>
|
||||
<div className={style.eventActions}>
|
||||
<CopyTag label='OSC trigger'>{`/ontime/gotoid/${event.id}`}</CopyTag>
|
||||
@@ -60,7 +58,6 @@ export default function EventEditor() {
|
||||
timerType={event.timerType}
|
||||
/>
|
||||
<EventEditorTitles
|
||||
key={event.id}
|
||||
eventId={event.id}
|
||||
title={event.title}
|
||||
presenter={event.presenter}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import { IconButton } from '@chakra-ui/react';
|
||||
import { Box, IconButton } from '@chakra-ui/react';
|
||||
import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
||||
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
@@ -26,7 +26,7 @@ const EventEditorExport = () => {
|
||||
const removeOpenEvent = () => setEditId(null);
|
||||
|
||||
return (
|
||||
<div className={editorStyle}>
|
||||
<Box className={editorStyle}>
|
||||
<ErrorBoundary>
|
||||
<div className={style.eventEditorLayout}>
|
||||
<EventEditor />
|
||||
@@ -35,7 +35,7 @@ const EventEditorExport = () => {
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -24,11 +24,10 @@ export default function CountedTextArea(props: CountedTextAreaProps) {
|
||||
return (
|
||||
<div className={`${style.column} ${style.fullHeight}`}>
|
||||
<div className={style.countedInput}>
|
||||
<label className={style.inputLabel} htmlFor={field}>{label}</label>
|
||||
<label className={style.inputLabel}>{label}</label>
|
||||
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||
</div>
|
||||
<Textarea
|
||||
id={field}
|
||||
size='sm'
|
||||
resize='none'
|
||||
variant='ontime-filled'
|
||||
|
||||
@@ -26,11 +26,10 @@ export default function CountedTextInput(props: CountedTextInputProps) {
|
||||
return (
|
||||
<div className={style.column}>
|
||||
<div className={style.countedInput}>
|
||||
<label className={style.inputLabel} htmlFor={field}>{label}</label>
|
||||
<label className={style.inputLabel}>{label}</label>
|
||||
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||
</div>
|
||||
<Input
|
||||
id={field}
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
data-testid='input-textfield'
|
||||
|
||||
@@ -80,11 +80,8 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
return (
|
||||
<div className={style.timeOptions}>
|
||||
<div className={style.timers}>
|
||||
<label className={inputTimeLabels} htmlFor='timeStart'>
|
||||
{startLabel}
|
||||
</label>
|
||||
<label className={inputTimeLabels}>{startLabel}</label>
|
||||
<TimeInput
|
||||
id='timeStart'
|
||||
name='timeStart'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={timerValidationHandler}
|
||||
@@ -93,11 +90,8 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
placeholder='Start'
|
||||
warning={warning.start}
|
||||
/>
|
||||
<label className={inputTimeLabels} htmlFor='timeEnd'>
|
||||
{endLabel}
|
||||
</label>
|
||||
<label className={inputTimeLabels}>{endLabel}</label>
|
||||
<TimeInput
|
||||
id='timeEnd'
|
||||
name='timeEnd'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={timerValidationHandler}
|
||||
@@ -106,11 +100,8 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
placeholder='End'
|
||||
warning={warning.end}
|
||||
/>
|
||||
<label className={style.inputLabel} htmlFor='durationOverride'>
|
||||
Duration
|
||||
</label>
|
||||
<label className={style.inputLabel}>Duration</label>
|
||||
<TimeInput
|
||||
id='durationOverride'
|
||||
name='durationOverride'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={timerValidationHandler}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
font-family: $ontime-font-family;
|
||||
color: $label-gray;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.labels {
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function Info() {
|
||||
return (
|
||||
<>
|
||||
<div className={style.panelHeader}>
|
||||
<span>Ontime running on port 4001</span>
|
||||
<span>{selected}</span>
|
||||
</div>
|
||||
<CollapsableInfo title='Network Info'>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
@@ -10,15 +11,15 @@ import style from '../editors/Editor.module.scss';
|
||||
|
||||
const InfoExport = () => {
|
||||
return (
|
||||
<div className={style.info} data-testid='panel-info'>
|
||||
<Box className={style.info} data-testid='panel-info'>
|
||||
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'info')} />
|
||||
<div className={style.content}>
|
||||
<ErrorBoundary>
|
||||
<Info />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default memo(InfoExport);
|
||||
export default memo(InfoExport)
|
||||
|
||||
@@ -102,7 +102,7 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} size='md' />
|
||||
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} />
|
||||
|
||||
<div className={style.gap} />
|
||||
<TooltipActionBtn
|
||||
@@ -118,17 +118,15 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
icon={<IoPushOutline />}
|
||||
className={isUploadOpen ? style.open : ''}
|
||||
clickHandler={onUploadOpen}
|
||||
tooltip='Import project file'
|
||||
aria-label='Import project file'
|
||||
size='sm'
|
||||
tooltip='Upload showfile'
|
||||
aria-label='Upload showfile'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<IoSaveOutline />}
|
||||
clickHandler={downloadRundown}
|
||||
tooltip='Export project file'
|
||||
aria-label='Export project file'
|
||||
size='sm'
|
||||
tooltip='Export showfile'
|
||||
aria-label='Export showfile'
|
||||
/>
|
||||
|
||||
<div className={style.gap} />
|
||||
@@ -139,7 +137,6 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
clickHandler={setRunMode}
|
||||
tooltip='Run mode'
|
||||
aria-label='Run mode'
|
||||
size='sm'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
@@ -148,7 +145,6 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
clickHandler={setEditMode}
|
||||
tooltip='Edit mode'
|
||||
aria-label='Edit mode'
|
||||
size='sm'
|
||||
/>
|
||||
|
||||
<div className={style.gap} />
|
||||
@@ -159,7 +155,6 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
clickHandler={onIntegrationOpen}
|
||||
tooltip='Integrations'
|
||||
aria-label='Integrations'
|
||||
size='sm'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
@@ -168,7 +163,6 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
clickHandler={onSettingsOpen}
|
||||
tooltip='Settings'
|
||||
aria-label='Settings'
|
||||
size='sm'
|
||||
/>
|
||||
<div className={style.gap} />
|
||||
<TooltipActionBtn
|
||||
@@ -178,7 +172,6 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
clickHandler={onAboutOpen}
|
||||
tooltip='About'
|
||||
aria-label='About'
|
||||
size='sm'
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ interface ModalWrapperProps {
|
||||
|
||||
export default function ModalWrapper(props: PropsWithChildren<ModalWrapperProps>) {
|
||||
const { isOpen, onClose, title, size = 'xl', children } = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClose}
|
||||
|
||||
@@ -12,7 +12,7 @@ interface IntegrationModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const oscDocsUrl = 'https://ontime.gitbook.io/v2/control-and-feedback/integrations';
|
||||
const oscDocsUrl = 'https://ontime.gitbook.io/v2/control-and-feedback/osc';
|
||||
|
||||
export default function IntegrationModal(props: IntegrationModalProps) {
|
||||
const { isOpen, onClose } = props;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { OscSubscription } from 'ontime-types';
|
||||
import type { OSCSettings, OscSubscription } from 'ontime-types';
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
import { generateId } from 'ontime-utils';
|
||||
|
||||
import useOscSettings, { usePostOscSubscriptions } from '../../../common/hooks-query/useOscSettings';
|
||||
import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-query/useOscSettings';
|
||||
import { oscPlaceholderSettings, PlaceholderSettings } from '../../../common/models/OscSettings';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
import OntimeModalFooter from '../OntimeModalFooter';
|
||||
|
||||
import OscSubscriptionRow from './OscSubscriptionRow';
|
||||
@@ -33,7 +34,7 @@ const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string
|
||||
},
|
||||
onUpdate: {
|
||||
title: 'On Every Second',
|
||||
subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)',
|
||||
subtitle: 'Triggers when timers are updated (at least once a second, can be more)',
|
||||
},
|
||||
onFinish: {
|
||||
title: 'On Finish',
|
||||
@@ -42,116 +43,88 @@ const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string
|
||||
};
|
||||
|
||||
export default function OscIntegration() {
|
||||
const { data, isFetching } = useOscSettings();
|
||||
const { mutateAsync } = usePostOscSubscriptions();
|
||||
const { data } = useOscSettings();
|
||||
const { mutateAsync } = useOscSettingsMutation();
|
||||
const { emitError } = useEmitLog();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
formState: { isSubmitting, isDirty, isValid },
|
||||
} = useForm<OscSubscription>({
|
||||
defaultValues: data.subscriptions,
|
||||
values: data.subscriptions,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
} = useForm<PlaceholderSettings>({
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
});
|
||||
|
||||
const [subscriptionState, setSubscription] = useState<OscSubscription>(
|
||||
data?.subscriptions || oscPlaceholderSettings.subscriptions,
|
||||
);
|
||||
const [hasManualChange, setHasManualChange] = useState(false);
|
||||
|
||||
const [showSection, setShowSection] = useState<OntimeCycle>(TimerLifeCycle.onLoad);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data.subscriptions);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const resetForm = () => {
|
||||
reset(data.subscriptions);
|
||||
const originalData = data || oscPlaceholderSettings;
|
||||
setSubscription(originalData.subscriptions);
|
||||
// @ts-expect-error -- we know the data here is safe
|
||||
reset(originalData);
|
||||
};
|
||||
|
||||
const onSubmit = async (values: OscSubscription) => {
|
||||
try {
|
||||
const subscriptions = {
|
||||
onLoad: values.onLoad ?? [],
|
||||
onStart: values.onStart ?? [],
|
||||
onPause: values.onPause ?? [],
|
||||
onStop: values.onStop ?? [],
|
||||
onUpdate: values.onUpdate ?? [],
|
||||
onFinish: values.onFinish ?? [],
|
||||
};
|
||||
const deleteSubscriptionEntry = (cycle: OntimeCycle, id: string) => {
|
||||
setSubscription((prev) => {
|
||||
const newData = { ...prev };
|
||||
newData[cycle] = [...prev[cycle].filter((el) => el.id !== id)];
|
||||
return newData;
|
||||
});
|
||||
setHasManualChange(true);
|
||||
};
|
||||
|
||||
await mutateAsync(subscriptions);
|
||||
const addNewSubscriptionEntry = async (cycle: OntimeCycle) => {
|
||||
setSubscription((prev) => {
|
||||
const newData = structuredClone(prev);
|
||||
newData[cycle] = [...prev[cycle], { id: generateId(), message: '', enabled: false }];
|
||||
return newData;
|
||||
});
|
||||
setHasManualChange(true);
|
||||
};
|
||||
|
||||
const onSubmit = async (values: OSCSettings | PlaceholderSettings) => {
|
||||
try {
|
||||
// @ts-expect-error -- we know of the type mismatch, not pertinent here
|
||||
await mutateAsync(values);
|
||||
setHasManualChange(false);
|
||||
} catch (error) {
|
||||
emitError(`Error setting OSC: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (isFetching) {
|
||||
return <ModalLoader />;
|
||||
}
|
||||
const subscriptionKeys = Object.keys(subscriptionState);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='osc-subscriptions'>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onLoad}
|
||||
title={sectionText.onLoad.title}
|
||||
subtitle={sectionText.onLoad.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onLoad}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onStart}
|
||||
title={sectionText.onStart.title}
|
||||
subtitle={sectionText.onStart.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onStart}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onPause}
|
||||
title={sectionText.onPause.title}
|
||||
subtitle={sectionText.onPause.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onPause}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onStop}
|
||||
title={sectionText.onStop.title}
|
||||
subtitle={sectionText.onStop.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onStop}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onUpdate}
|
||||
title={sectionText.onUpdate.title}
|
||||
subtitle={sectionText.onUpdate.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onUpdate}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
/>
|
||||
<OscSubscriptionRow
|
||||
cycle={TimerLifeCycle.onFinish}
|
||||
title={sectionText.onFinish.title}
|
||||
subtitle={sectionText.onFinish.subtitle}
|
||||
visible={showSection === TimerLifeCycle.onFinish}
|
||||
setShowSection={setShowSection}
|
||||
register={register}
|
||||
control={control}
|
||||
/>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSubscriptions'>
|
||||
{subscriptionKeys.map((cycle, idx) => {
|
||||
return (
|
||||
<div key={`${cycle}-${idx}`}>
|
||||
<OscSubscriptionRow
|
||||
key={cycle}
|
||||
cycle={cycle as TimerLifeCycle}
|
||||
title={sectionText[cycle as TimerLifeCycle].title}
|
||||
subtitle={sectionText[cycle as TimerLifeCycle].subtitle}
|
||||
visible={showSection === cycle}
|
||||
setShowSection={setShowSection}
|
||||
subscriptionOptions={subscriptionState[cycle as TimerLifeCycle]}
|
||||
handleDelete={deleteSubscriptionEntry}
|
||||
handleAddNew={addNewSubscriptionEntry}
|
||||
register={register}
|
||||
/>
|
||||
{idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<OntimeModalFooter
|
||||
formId='osc-subscriptions'
|
||||
formId='oscSubscriptions'
|
||||
handleRevert={resetForm}
|
||||
isDirty={isDirty}
|
||||
isDirty={isDirty || hasManualChange}
|
||||
isValid={isValid}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FormControl, Input, Switch } from '@chakra-ui/react';
|
||||
|
||||
@@ -6,13 +5,12 @@ import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-qu
|
||||
import { PlaceholderSettings } from '../../../common/models/OscSettings';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import { isIPAddress, isOnlyNumbers } from '../../../common/utils/regex';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
import OntimeModalFooter from '../OntimeModalFooter';
|
||||
|
||||
import styles from '../Modal.module.scss';
|
||||
|
||||
export default function OscSettings() {
|
||||
const { data, isFetching } = useOscSettings();
|
||||
const { data } = useOscSettings();
|
||||
const { mutateAsync } = useOscSettingsMutation();
|
||||
const { emitError } = useEmitLog();
|
||||
const {
|
||||
@@ -24,16 +22,8 @@ export default function OscSettings() {
|
||||
} = useForm<PlaceholderSettings>({
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
const onSubmit = async (values: PlaceholderSettings) => {
|
||||
const numericPortIn = Number(values.portIn);
|
||||
const numericPortOut = Number(values.portOut);
|
||||
@@ -61,10 +51,6 @@ export default function OscSettings() {
|
||||
reset(data);
|
||||
};
|
||||
|
||||
if (isFetching) {
|
||||
return <ModalLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSettings'>
|
||||
<div className={styles.splitSection}>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Control, useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { OscSubscription, TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import { OscSubscriptionOptions, TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import collapseStyles from '../../../common/components/collapse-bar/CollapseBar.module.scss';
|
||||
import styles from '../Modal.module.scss';
|
||||
@@ -15,50 +13,37 @@ interface OscSubscriptionRowProps {
|
||||
subtitle: string;
|
||||
visible: boolean;
|
||||
setShowSection: (cycle: TimerLifeCycle) => void;
|
||||
register: UseFormRegister<OscSubscription>;
|
||||
control: Control<OscSubscription>;
|
||||
subscriptionOptions: OscSubscriptionOptions[];
|
||||
handleDelete: (cycle: TimerLifeCycle, id: string) => void;
|
||||
handleAddNew: (cycle: TimerLifeCycle) => void;
|
||||
register: UseFormRegister<any>;
|
||||
}
|
||||
|
||||
export default function OscSubscriptionRow(props: OscSubscriptionRowProps) {
|
||||
const { cycle, title, subtitle, visible, setShowSection, register, control } = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: cycle,
|
||||
control,
|
||||
});
|
||||
const { cycle, title, subtitle, visible, setShowSection, subscriptionOptions, handleDelete, handleAddNew, register } =
|
||||
props;
|
||||
|
||||
const hasTooManyOptions = fields.length >= 3;
|
||||
const hasTooManyOptions = subscriptionOptions.length >= 3;
|
||||
const headerStyle = `${styles.splitSection} ${visible ? '' : styles.showPointer}`;
|
||||
|
||||
const sectionTitle = `${title} ${fields.length ? fields.length : '-'} / 3`;
|
||||
|
||||
const handleAddNew = () => {
|
||||
if (hasTooManyOptions) {
|
||||
emitError('Maximum amount of onLoad subscriptions reached (3)');
|
||||
return;
|
||||
}
|
||||
append({
|
||||
message: '',
|
||||
enabled: false,
|
||||
});
|
||||
};
|
||||
const registerPrefix = `subscriptions.${cycle}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={headerStyle} onClick={() => setShowSection(cycle)}>
|
||||
<div>
|
||||
<span className={`${styles.sectionTitle} ${styles.main}`}>{sectionTitle}</span>
|
||||
<span className={`${styles.sectionTitle} ${styles.main}`}>{title}</span>
|
||||
{visible && <span className={styles.sectionSubtitle}>{subtitle}</span>}
|
||||
</div>
|
||||
<FiChevronUp className={visible ? collapseStyles.moreCollapsed : collapseStyles.moreExpanded} />
|
||||
</div>
|
||||
{visible && (
|
||||
<>
|
||||
{fields.map((subscription, index) => (
|
||||
<div key={subscription.id} className={styles.entryRow}>
|
||||
{subscriptionOptions.map((option, idx) => (
|
||||
<div key={option.id} className={styles.entryRow}>
|
||||
<input type='hidden' {...register(`${registerPrefix}[${idx}].id`)} value={option.id} />
|
||||
<IconButton
|
||||
icon={<IoRemove />}
|
||||
onClick={() => remove(index)}
|
||||
onClick={() => handleDelete(cycle, option.id)}
|
||||
aria-label='delete'
|
||||
size='xs'
|
||||
colorScheme='red'
|
||||
@@ -67,14 +52,13 @@ export default function OscSubscriptionRow(props: OscSubscriptionRowProps) {
|
||||
placeholder='OSC Message'
|
||||
size='xs'
|
||||
variant='ontime-filled-on-light'
|
||||
autoComplete='off'
|
||||
{...register(`${cycle}.${index}.message`)}
|
||||
{...register(`${registerPrefix}[${idx}].message`)}
|
||||
/>
|
||||
<Switch variant='ontime-on-light' {...register(`${cycle}.${index}.enabled`)} />
|
||||
<Switch variant='ontime-on-light' {...register(`${registerPrefix}[${idx}].enabled`)} />
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
onClick={handleAddNew}
|
||||
onClick={() => handleAddNew(cycle)}
|
||||
className={styles.shiftRight}
|
||||
isDisabled={hasTooManyOptions}
|
||||
size='xs'
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
@use '../../../theme/ontimeColours' as *;
|
||||
|
||||
.screenLoader {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
top: 0;
|
||||
background-color: $white-60;
|
||||
}
|
||||
|
||||
$loader-size: 48px;
|
||||
|
||||
.loader {
|
||||
width: $loader-size;
|
||||
height: $loader-size;
|
||||
background: $blue-500;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
animation: animloader 1s ease-in infinite;
|
||||
}
|
||||
|
||||
@keyframes animloader {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animloader {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import style from './ModalLoader.module.scss';
|
||||
|
||||
export default function ModalLoader() {
|
||||
return (
|
||||
<div className={style.screenLoader}>
|
||||
<span className={style.loader} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,3 +2,10 @@ export const inputProps = {
|
||||
size: 'sm',
|
||||
autoComplete: 'off',
|
||||
};
|
||||
|
||||
export const portInputProps = {
|
||||
...inputProps,
|
||||
type: 'number',
|
||||
min: '1024',
|
||||
max: '65535',
|
||||
};
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
@@ -38,27 +34,16 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
|
||||
register,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
} = useForm({
|
||||
defaultValues: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
} = useForm({ defaultValues: data });
|
||||
|
||||
useEffect(() => {
|
||||
if (data) reset(data);
|
||||
}, [data, reset]);
|
||||
reset(data);
|
||||
}, [reset, data]);
|
||||
|
||||
const onSubmit = async (data: Partial<EventData>) => {
|
||||
try {
|
||||
await postNew(data);
|
||||
await ontimeQueryClient.invalidateQueries(EVENT_DATA);
|
||||
await ontimeQueryClient.invalidateQueries(RUNDOWN_TABLE);
|
||||
|
||||
onClose();
|
||||
} catch (_) {
|
||||
/* WE DO NOT HANDLE ERRORS */
|
||||
}
|
||||
await postNew(data);
|
||||
await ontimeQueryClient.invalidateQueries(EVENT_DATA);
|
||||
await ontimeQueryClient.invalidateQueries(RUNDOWN_TABLE);
|
||||
};
|
||||
|
||||
const onReset = () => reset(eventDataPlaceholder);
|
||||
@@ -81,15 +66,6 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
|
||||
<ModalCloseButton />
|
||||
<ModalBody className={styles.pad}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer}>
|
||||
<Alert status='info' variant='ontime-on-light-info'>
|
||||
<AlertIcon />
|
||||
<div className={styles.column}>
|
||||
<AlertTitle>Note</AlertTitle>
|
||||
<AlertDescription>
|
||||
On submit, application options will be kept but rundown and event data will be reset
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</Alert>
|
||||
<div className={styles.entryRow}>
|
||||
<label className={styles.sectionTitle}>
|
||||
Event title
|
||||
@@ -148,6 +124,9 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.footerNotes}>
|
||||
Note: Application options will be kept but rundown and event data will be reset <br />
|
||||
</div>
|
||||
<ModalFooter className={styles.buttonSection}>
|
||||
<Button onClick={onReset} isDisabled={disableButtons} variant='ontime-ghost-on-light' size='sm'>
|
||||
Clear data
|
||||
@@ -160,7 +139,7 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
|
||||
padding='0 2em'
|
||||
size='sm'
|
||||
>
|
||||
New project file
|
||||
New showfile
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||
import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
|
||||
@@ -10,7 +9,6 @@ import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBt
|
||||
import useAliases from '../../../common/hooks-query/useAliases';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import { handleLinks } from '../../../common/utils/linkUtils';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
import { inputProps } from '../modalHelper';
|
||||
import ModalLink from '../ModalLink';
|
||||
import OntimeModalFooter from '../OntimeModalFooter';
|
||||
@@ -23,7 +21,7 @@ const aliasesDocsUrl = 'https://ontime.gitbook.io/v2/features/url-aliases';
|
||||
type Aliases = { aliases: Alias[] };
|
||||
|
||||
export default function AliasesForm() {
|
||||
const { data, status, isFetching, refetch } = useAliases();
|
||||
const { data, status, refetch } = useAliases();
|
||||
const { emitError } = useEmitLog();
|
||||
const {
|
||||
control,
|
||||
@@ -34,21 +32,12 @@ export default function AliasesForm() {
|
||||
} = useForm<Aliases>({
|
||||
defaultValues: { aliases: data },
|
||||
values: { aliases: data || [] },
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'aliases',
|
||||
control,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = async (formData: Aliases) => {
|
||||
try {
|
||||
await postAliases(formData.aliases);
|
||||
@@ -65,7 +54,7 @@ export default function AliasesForm() {
|
||||
|
||||
const addNew = () => {
|
||||
if (fields.length > 20) {
|
||||
emitError('Maximum amount of aliases reached (20)');
|
||||
emitError('Maximum amount of aliases reacted (20)');
|
||||
return;
|
||||
}
|
||||
append({
|
||||
@@ -78,10 +67,6 @@ export default function AliasesForm() {
|
||||
const disableInputs = status === 'loading';
|
||||
const hasTooManyOptions = fields.length >= 20;
|
||||
|
||||
if (isFetching) {
|
||||
return <ModalLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} id='aliases' className={style.sectionContainer}>
|
||||
<div style={{ height: '16px' }} />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Input, Select } from '@chakra-ui/react';
|
||||
import type { Settings } from 'ontime-types';
|
||||
@@ -7,7 +6,6 @@ import { postSettings } from '../../../common/api/ontimeApi';
|
||||
import useSettings from '../../../common/hooks-query/useSettings';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import { isOnlyNumbers } from '../../../common/utils/regex';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
import ModalSplitInput from '../ModalSplitInput';
|
||||
import OntimeModalFooter from '../OntimeModalFooter';
|
||||
|
||||
@@ -16,7 +14,7 @@ import ModalPinInput from './ModalPinInput';
|
||||
import style from './SettingsModal.module.scss';
|
||||
|
||||
export default function AppSettingsModal() {
|
||||
const { data, status, isFetching, refetch } = useSettings();
|
||||
const { data, status, refetch } = useSettings();
|
||||
const { emitError } = useEmitLog();
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -26,17 +24,8 @@ export default function AppSettingsModal() {
|
||||
} = useForm<Settings>({
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = async (formData: Settings) => {
|
||||
try {
|
||||
await postSettings(formData);
|
||||
@@ -53,10 +42,6 @@ export default function AppSettingsModal() {
|
||||
|
||||
const disableInputs = status === 'loading';
|
||||
|
||||
if (isFetching) {
|
||||
return <ModalLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} id='app-settings' className={style.sectionContainer}>
|
||||
<ModalSplitInput
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Input } from '@chakra-ui/react';
|
||||
import { UserFields } from 'ontime-types';
|
||||
@@ -6,7 +5,6 @@ import { UserFields } from 'ontime-types';
|
||||
import { postUserFields } from '../../../common/api/ontimeApi';
|
||||
import useUserFields from '../../../common/hooks-query/useUserFields';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
import { inputProps } from '../modalHelper';
|
||||
import ModalLink from '../ModalLink';
|
||||
import ModalSplitInput from '../ModalSplitInput';
|
||||
@@ -17,7 +15,7 @@ import style from './SettingsModal.module.scss';
|
||||
const userFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields';
|
||||
|
||||
export default function CuesheetSettings() {
|
||||
const { data, status, isFetching, refetch } = useUserFields();
|
||||
const { data, status, refetch } = useUserFields();
|
||||
const { emitError } = useEmitLog();
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -27,17 +25,8 @@ export default function CuesheetSettings() {
|
||||
} = useForm<UserFields>({
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = async (formData: UserFields) => {
|
||||
try {
|
||||
await postUserFields(formData);
|
||||
@@ -54,10 +43,6 @@ export default function CuesheetSettings() {
|
||||
|
||||
const disableInputs = status === 'loading';
|
||||
|
||||
if (isFetching) {
|
||||
return <ModalLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} id='cuesheet-settings' className={style.sectionContainer}>
|
||||
<div style={{ height: '16px' }} />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Input, Textarea } from '@chakra-ui/react';
|
||||
import { EventData } from 'ontime-types';
|
||||
@@ -6,7 +5,6 @@ import { EventData } from 'ontime-types';
|
||||
import { postEventData } from '../../../common/api/eventDataApi';
|
||||
import useEventData from '../../../common/hooks-query/useEventData';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
import { inputProps } from '../modalHelper';
|
||||
import ModalInput from '../ModalInput';
|
||||
import OntimeModalFooter from '../OntimeModalFooter';
|
||||
@@ -14,7 +12,7 @@ import OntimeModalFooter from '../OntimeModalFooter';
|
||||
import style from './SettingsModal.module.scss';
|
||||
|
||||
export default function EventDataForm() {
|
||||
const { data, status, isFetching, refetch } = useEventData();
|
||||
const { data, status, refetch } = useEventData();
|
||||
const { emitError } = useEmitLog();
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -24,17 +22,8 @@ export default function EventDataForm() {
|
||||
} = useForm<EventData>({
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = async (formData: EventData) => {
|
||||
try {
|
||||
await postEventData(formData);
|
||||
@@ -51,10 +40,6 @@ export default function EventDataForm() {
|
||||
|
||||
const disableInputs = status === 'loading';
|
||||
|
||||
if (isFetching) {
|
||||
return <ModalLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} id='event-data' className={style.sectionContainer}>
|
||||
<ModalInput
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user