mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-03 06:28:01 +00:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82d0508672 | |||
| dd1b9bd38c | |||
| b01145ca4b | |||
| 67ddcd8c68 | |||
| 8841a02754 | |||
| 1a0d7810c4 | |||
| f2dd24b74a | |||
| 6a6f68b2f9 | |||
| 906a6631a7 | |||
| 79a24a134e | |||
| acb5d8ae08 | |||
| 5ca8202368 | |||
| ba782d5d22 | |||
| 48093b8651 | |||
| c3f18feaae | |||
| 2fa397548a | |||
| dd43d5e20a | |||
| 9fc154955a | |||
| 160ccabebc | |||
| 4974898050 | |||
| ac0d5832b6 | |||
| 2b2bafa8c6 | |||
| fad34eeab2 | |||
| c12bca05aa | |||
| 1e4206fcb2 | |||
| 2751ed3d33 | |||
| cd911aaaa2 | |||
| 1321aa555e | |||
| 12e4eaef67 | |||
| 28c9cb0864 | |||
| afdfae9073 | |||
| 0736ce93c6 | |||
| 52f02f153c | |||
| 3582ce18e1 | |||
| 80c2e9a8ba | |||
| 627cce7ba6 | |||
| 8c740b7111 | |||
| bf8170a947 | |||
| eb2abb7026 | |||
| f3320efc5c | |||
| 17d5c84133 | |||
| 09a5aca25a | |||
| 885d965db1 | |||
| 26cb8a5bfc | |||
| 8520d08927 | |||
| 8d8bbef546 | |||
| 207612e3b8 | |||
| 98e5cddad5 | |||
| f6edb88944 | |||
| 3d2a79f92a | |||
| f73d8ae8f4 | |||
| 1138b4dd98 | |||
| ad89a780be | |||
| 9bc2923586 | |||
| 93580554bd | |||
| 03ef3e58ed | |||
| 4d2769dee4 | |||
| 0bb728d76c | |||
| 5a68d292e1 | |||
| 418f8018fe | |||
| 89d3bc967c | |||
| 144ce1c046 | |||
| 513e50a998 | |||
| f0ecb85c29 | |||
| c0e0631cd2 | |||
| dca7533a2b | |||
| 8fcf33933e | |||
| 1c327cde1f | |||
| 3a85a483c1 | |||
| ba3f0372dd | |||
| 31f1786eae | |||
| 6648586c26 | |||
| e4b712593d | |||
| 1305fad6b5 | |||
| e1f9d322da | |||
| 1ffd6ff7b0 | |||
| c8e4704daf | |||
| b08bc64bd4 | |||
| 4dc7198c52 | |||
| 7d99301beb | |||
| f3a4e6405b | |||
| 0bf663b149 | |||
| 167174a914 | |||
| 4ed385dd08 | |||
| 1afdd67760 | |||
| 0a5fa12c21 | |||
| 6b5fb186cc | |||
| b2a1974a84 | |||
| 218c94ebd6 | |||
| 13487867bc | |||
| 1cad360f55 | |||
| a237537366 | |||
| ec714b341f | |||
| f999f19aa9 | |||
| a9641f0c54 | |||
| a70cb23242 | |||
| 5a87668fac | |||
| 23ef4ce155 | |||
| 21c6303e87 | |||
| d4cf983d5d | |||
| d03424d5c4 | |||
| 0977ee6683 | |||
| affeb75bcd | |||
| 3dc8483d4f | |||
| 4f5d13214b | |||
| 33d8cbef08 | |||
| 541117b31b | |||
| 5c659758b1 | |||
| b62d376e64 | |||
| 41e2b88376 | |||
| 310579143e | |||
| a0f3390ad3 | |||
| abdd2606ee | |||
| d36adf08ba | |||
| a638e5962c | |||
| 759dece70f | |||
| 6ca2eb77a1 | |||
| 8335af4ed0 | |||
| de44ea81d3 | |||
| 260b0fc97f | |||
| 06ab1185c9 |
@@ -0,0 +1,18 @@
|
||||
version = 1
|
||||
|
||||
test_patterns = [
|
||||
"__mocks__/**",
|
||||
"__tests__/**",
|
||||
"cypress/**",
|
||||
"*.test.*",
|
||||
"*.mock.*",
|
||||
"*.spec.*"
|
||||
]
|
||||
|
||||
[[analyzers]]
|
||||
name = "javascript"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
plugins = ["react"]
|
||||
environment = ["nodejs","browser","jest"]
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {
|
||||
// disallow certain object properties
|
||||
// https://eslint.org/docs/rules/no-restricted-properties
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
{
|
||||
"object": "global",
|
||||
"property": "isNaN",
|
||||
"message": "Please use Number.isNaN instead"
|
||||
},
|
||||
{
|
||||
"object": "self",
|
||||
"property": "isNaN",
|
||||
"message": "Please use Number.isNaN instead"
|
||||
},
|
||||
{
|
||||
"object": "window",
|
||||
"property": "isNaN",
|
||||
"message": "Please use Number.isNaN instead"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
@@ -40,8 +40,13 @@ jobs:
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# install node dependencies
|
||||
- name: Install nodejs dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# install and build electron
|
||||
- name: Install Electron + nodejs dependencies
|
||||
- name: Install Electron dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server
|
||||
- name: Build Electron App
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '33 02 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# Utils server
|
||||
- name: Utils - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/utils
|
||||
|
||||
- name: Utils - run link command
|
||||
run: yarn link
|
||||
working-directory: ./server/utils
|
||||
|
||||
# React
|
||||
- name: React - Link to utils
|
||||
run: yarn link ontime-utils
|
||||
working-directory: ./client
|
||||
|
||||
- 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: React - Link to utils
|
||||
run: yarn link ontime-utils
|
||||
working-directory: ./server/src
|
||||
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
run: yarn install && yarn make && yarn setdb
|
||||
working-directory: ./server
|
||||
|
||||
- name: Electron - Run tests
|
||||
run: yarn test
|
||||
working-directory: ./server
|
||||
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
working-directory: ./server
|
||||
start: yarn cypress
|
||||
+12
-7
@@ -6,7 +6,8 @@ node_modules/
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
coverage/
|
||||
*.mp4
|
||||
|
||||
# production
|
||||
build/
|
||||
@@ -23,13 +24,17 @@ dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
ontime.code-workspace
|
||||
TODO.md
|
||||
|
||||
# working stuff
|
||||
_SS/
|
||||
.vscode/launch.json
|
||||
db.json
|
||||
.eslintrc.json
|
||||
db.json
|
||||
db backup.json
|
||||
server/src/data/db.json
|
||||
server/src/models/db.json
|
||||
TODO.md
|
||||
|
||||
# vscode stuff
|
||||
.vscode/*
|
||||
ontime.code-workspace
|
||||
|
||||
# webstorm stuff
|
||||
.idea/*
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myCustomPropertiesEnabled" value="true" />
|
||||
<option name="myIgnoreVendorSpecificProperties" value="false" />
|
||||
<option name="myCustomPropertiesList">
|
||||
<value>
|
||||
<list size="3">
|
||||
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
|
||||
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
|
||||
<item index="2" class="java.lang.String" itemvalue="app-region" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredUrls">
|
||||
<list>
|
||||
<option value="http://localhost" />
|
||||
<option value="http://127.0.0.1" />
|
||||
<option value="http://0.0.0.0" />
|
||||
<option value="http://www.w3.org/" />
|
||||
<option value="http://json-schema.org/draft" />
|
||||
<option value="http://java.sun.com/" />
|
||||
<option value="http://xmlns.jcp.org/" />
|
||||
<option value="http://javafx.com/javafx/" />
|
||||
<option value="http://javafx.com/fxml" />
|
||||
<option value="http://maven.apache.org/xsd/" />
|
||||
<option value="http://maven.apache.org/POM/" />
|
||||
<option value="http://www.springframework.org/schema/" />
|
||||
<option value="http://www.springframework.org/tags" />
|
||||
<option value="http://www.springframework.org/security/tags" />
|
||||
<option value="http://www.thymeleaf.org" />
|
||||
<option value="http://www.jboss.org/j2ee/schema/" />
|
||||
<option value="http://www.jboss.com/xml/ns/" />
|
||||
<option value="http://www.ibm.com/webservices/xsd" />
|
||||
<option value="http://activemq.apache.org/schema/" />
|
||||
<option value="http://schema.cloudfoundry.org/spring/" />
|
||||
<option value="http://schemas.xmlsoap.org/" />
|
||||
<option value="http://cxf.apache.org/schemas/" />
|
||||
<option value="http://primefaces.org/ui" />
|
||||
<option value="http://tiles.apache.org/" />
|
||||
<option value="http://__IP__" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 201 KiB |
@@ -1,13 +1,47 @@
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://cpvalente.gitbook.io/ontime/)
|
||||
|
||||
|
||||
Download the latest releases here
|
||||
- [Windows](https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe)
|
||||
|
||||
# Ontime
|
||||
Ontime is an application for managing event rundowns and running stage timers.
|
||||
|
||||
It allows a center application to be able to distribute event information in the local network. This minimises needs for using Media Server outputs or expensive video distribution while allowing easy integration in workflows including OBS and d3.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## Using Ontime
|
||||
Once installed and running, ontime starts a background server that is the heart of all processes.
|
||||
The app, is used to add / edit your running order in the event list, and running the timers using the Playback Control function.
|
||||
|
||||
From here, any device in the same network with a browser is able to render the views as described. This is done by reaching the ontime server at the _default port 4001_ eg: `localhost:4001` or `192.168.1.3:4001`
|
||||
You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden).
|
||||
|
||||
In case of unattended machines or automations, it is possible to use different URL to recall individual views
|
||||
and extend with using the URL aliases feature
|
||||
|
||||
```
|
||||
For the presentation views...
|
||||
-------------------------------------------------------------
|
||||
IP.ADDRESS:4001 > Web server default to presenter timer view
|
||||
IP.ADDRESS:4001/presenter > Presenter / Stage timer view
|
||||
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
|
||||
IP.ADDRESS:4001/public > Public / Foyer view
|
||||
IP.ADDRESS:4001/pip > Picture in Picture view
|
||||
IP.ADDRESS:4001/lower > Lower Thirds
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
IP.ADDRESS:4001/cuesheet > Cue Sheet
|
||||
|
||||
...and for the editor (the control interface, same as the app)
|
||||
-------------------------------------------------------------
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
|
||||
```
|
||||
|
||||
More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
## Feature List (in no specific order)
|
||||
- [x] Distribute Data over network and render in the browser
|
||||
- [x] Different screen types
|
||||
@@ -15,37 +49,61 @@ It allows a center application to be able to distribute event information in the
|
||||
- Backstage Info
|
||||
- Public Info
|
||||
- Picture in Picture
|
||||
- Studio Clock
|
||||
- [x] Configurable realtime Lower Thirds
|
||||
- [x] Cuesheets with additional custom fields
|
||||
- [x] Send live messages to different screen types
|
||||
- [x] Ability to differentiate between backstage and public data
|
||||
- [x] Manage delays workflow
|
||||
- [x] OSC Control and Feedback
|
||||
- [x] Roll mode: run independently from system clock
|
||||
- [x] Open Sound Control (OSC) Control and Feedback
|
||||
- [x] Roll mode: run independently using the system clock
|
||||
- [x] Import event list from Excel
|
||||
- [x] URL Aliases (define configurable aliases to ease onsite setup)
|
||||
- [x] Logging view
|
||||
- [x] Edit anywhere: run ontime in your local network and use any machine to reach the editor page (same as app)
|
||||
|
||||
## Unopinionated
|
||||
We are not interested in forcing workflows and have made ontime so it is flexible to whichever way you would like to work.
|
||||
|
||||
We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work.
|
||||
|
||||
- [x] You do not need an order list to use the timer. Create an empty event and the OSC API works just the same
|
||||
- [x] If you want just the info screens, no need to use the timer!
|
||||
- [x] Dont have or care for a schedule?
|
||||
- [x] Don't have or care for a schedule?
|
||||
- [x] a single event with no data is enough to use the OSC API and get going
|
||||
- [x] use the order list to create a set of quick timers by setting the beggining and start times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quick recall this with OSC as always
|
||||
- [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 as always
|
||||
|
||||
## Integrations and Workflow
|
||||
The app is being currently developed to a wide user base, from broadcast to entertainment and conference halls.
|
||||
|
||||
Taking advantage of the integrations in Ontime, we currently use Ontime with:
|
||||
- `disguise`: trigger ontime from d3's timeline using the **OSC API**, **render views** using d3's webmodule
|
||||
- `OBS`: **render views** using the Browser Module
|
||||
- `QLab`: trigger ontime using **OSC API**
|
||||
- `Companion`: trigger ontime and manipulate timer using **OSC API**
|
||||
|
||||
|
||||
## Roadmap
|
||||
### Continued development
|
||||
There are several features planned in the roadmap.
|
||||
These will be implemented in a development friendly order unless there is user demand to bump any of them.
|
||||
- [ ] Linux version
|
||||
- [ ] Headless version (run server only anywhere, configure from a browser locally)
|
||||
- [ ] Companion module
|
||||
- [ ] Lower Third Manager
|
||||
- [ ] Note only event
|
||||
- [ ] Reach Schedule: way to speedup timer to meet a deadline
|
||||
- [ ] vMix integration
|
||||
|
||||
### For version 1
|
||||
Almost reaching a feature set that we can call v1. Before that:
|
||||
- [ ] Mac OS version
|
||||
- [ ] Finish Documentation
|
||||
### Continuing
|
||||
- [ ] Linux version
|
||||
- [ ] Headless version (run server only anywhere, configure from a browser locally)
|
||||
- [ ] Companion integration
|
||||
- [ ] Lower Third Manager
|
||||
- [ ] Note only event
|
||||
- [ ] URL Aliases (define configurable aliases to ease onsite setup)
|
||||
- [ ] Logging view
|
||||
- [ ] Reach Schedule: way to speedup timer to meet a deadline
|
||||
|
||||
### Issues
|
||||
The app is still in pre-release and there are a few issues, mainly concerning style.
|
||||
This will be receiving attention as we near v1 release
|
||||
|
||||
#### Style
|
||||
- [ ] App needs improvement on handling zoomed interfaces: Please ensure that windows settings have no display zoom (it is 125% by default)
|
||||
- [ ] Very long titles might cause interface to shift
|
||||
|
||||
# Help
|
||||
Help is underway! ... and can be viewed [here](https://cpvalente.gitbook.io/ontime/)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"testing-library",
|
||||
"jest"
|
||||
],
|
||||
"rules": {
|
||||
"jest/no-mocks-import": "warn",
|
||||
"no-useless-concat": "warn",
|
||||
"prefer-template": "warn",
|
||||
"react/jsx-no-bind": [
|
||||
"error",
|
||||
{
|
||||
"ignoreRefs": true,
|
||||
"allowArrowFunctions": true,
|
||||
"allowFunctions": false,
|
||||
"allowBind": false,
|
||||
"ignoreDOMComponents": true
|
||||
}
|
||||
],
|
||||
"react/jsx-boolean-value": [
|
||||
"error",
|
||||
"never",
|
||||
{
|
||||
"always": []
|
||||
}
|
||||
],
|
||||
"react/jsx-handler-names": [
|
||||
"off",
|
||||
{
|
||||
"eventHandlerPrefix": "handle",
|
||||
"eventHandlerPropPrefix": "on"
|
||||
}
|
||||
],
|
||||
"react/no-danger": "warn",
|
||||
"react/no-deprecated": [
|
||||
"error"
|
||||
],
|
||||
"react/jsx-no-undef": "error",
|
||||
"react/jsx-closing-tag-location": "warn",
|
||||
"react/jsx-no-useless-fragment": "warn",
|
||||
"react/no-unescaped-entities": "error",
|
||||
"react/jsx-tag-spacing": [
|
||||
"error",
|
||||
{
|
||||
"closingSlash": "never",
|
||||
"beforeSelfClosing": "always",
|
||||
"afterOpening": "never",
|
||||
"beforeClosing": "never"
|
||||
}
|
||||
],
|
||||
"react/jsx-space-before-closing": [
|
||||
"off",
|
||||
"always"
|
||||
],
|
||||
"react/jsx-curly-brace-presence": [
|
||||
"error",
|
||||
{
|
||||
"props": "never",
|
||||
"children": "never"
|
||||
}
|
||||
],
|
||||
"react/destructuring-assignment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"react/no-unsafe": "off",
|
||||
"react/prop-types": [
|
||||
"error",
|
||||
{
|
||||
"ignore": [],
|
||||
"customValidators": [],
|
||||
"skipUndeclared": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+32
-28
@@ -3,43 +3,38 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.4.1",
|
||||
"@emotion/react": "^11",
|
||||
"@emotion/styled": "^11",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"axios": "^0.21.1",
|
||||
"date-fns": "^2.20.1",
|
||||
"framer-motion": "^4.1.6",
|
||||
"jotai": "^0.16.5",
|
||||
"react": "^17.0.1",
|
||||
"@chakra-ui/react": "^1.8.6",
|
||||
"@dnd-kit/core": "^5.0.3",
|
||||
"@dnd-kit/sortable": "^6.0.1",
|
||||
"@dnd-kit/utilities": "^3.1.0",
|
||||
"@emotion/react": "^11.7.1",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"autosize": "^5.0.1",
|
||||
"axios": "^0.25.0",
|
||||
"color": "^4.2.1",
|
||||
"framer-motion": "4.1.17",
|
||||
"ontime-utils": "link: ../server/utils/",
|
||||
"react": "17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-qr-code": "^1.1.1",
|
||||
"react-query": "^3.13.5",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"socket.io-client": "^4.0.1",
|
||||
"tinykeys": "^1.1.2",
|
||||
"react-qr-code": "2.0.3",
|
||||
"react-query": "^3.34.12",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-table": "^7.7.0",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"typeface-open-sans": "^1.1.13",
|
||||
"web-vitals": "^1.0.1"
|
||||
"use-fit-text": "^2.4.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "set BROWSER=none&&react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
@@ -52,5 +47,14 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {}
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-scripts": "^5.0.0",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"sass": "^1.49.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,21 @@
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#121212" />
|
||||
<meta
|
||||
name="onsite"
|
||||
content="onsite - event timer manager"
|
||||
/>
|
||||
<meta name="onsite" content="onsite - event timer manager" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest">
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="%PUBLIC_URL%/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body,
|
||||
html,
|
||||
.App {
|
||||
margin: 0px auto;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
height: 100vh;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
+110
-47
@@ -1,63 +1,126 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import './App.css';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import SocketProvider from 'app/context/socketContext';
|
||||
import React, { lazy, Suspense, useCallback, useEffect } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import './App.scss';
|
||||
import withSocket from 'features/viewers/ViewWrapper';
|
||||
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
import ProtectRoute from './common/components/protectRoute/ProtectRoute';
|
||||
import { useFetch } from './app/hooks/useFetch';
|
||||
import { ALIASES } from './app/api/apiConstants';
|
||||
import { getAliases } from './app/api/ontimeApi';
|
||||
import { TableSettingsProvider } from './app/context/TableSettingsContext';
|
||||
|
||||
const Editor = lazy(() => import('features/editors/Editor'));
|
||||
const PresenterView = lazy(() =>
|
||||
import('features/viewers/presenter/PresenterView')
|
||||
);
|
||||
const PresenterSimple = lazy(() =>
|
||||
import('features/viewers/presenter/PresenterSimple')
|
||||
);
|
||||
const StageManager = lazy(() =>
|
||||
import('features/viewers/backstage/StageManager')
|
||||
);
|
||||
const Table = lazy(() => import('features/table/TableWrapper'));
|
||||
|
||||
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
|
||||
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer'));
|
||||
|
||||
const StageManager = lazy(() => import('features/viewers/backstage/StageManager'));
|
||||
const Public = lazy(() => import('features/viewers/foh/Public'));
|
||||
const Lower = lazy(() =>
|
||||
import('features/viewers/production/lower/LowerWrapper')
|
||||
);
|
||||
const Lower = lazy(() => import('features/viewers/production/lower/LowerWrapper'));
|
||||
const Pip = lazy(() => import('features/viewers/production/Pip'));
|
||||
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
// Seemed to cause issues
|
||||
// broadcastQueryClient({
|
||||
// queryClient,
|
||||
// broadcastChannel: 'ontime',
|
||||
// });
|
||||
|
||||
const SSpeaker = withSocket(PresenterView);
|
||||
const SSpeakerSimple = withSocket(PresenterSimple);
|
||||
const STimer = withSocket(TimerView);
|
||||
const SMinimalTimer = withSocket(MinimalTimerView);
|
||||
const SStageManager = withSocket(StageManager);
|
||||
const SPublic = withSocket(Public);
|
||||
const SLowerThird = withSocket(Lower);
|
||||
const SPip = withSocket(Pip);
|
||||
const SStudio = withSocket(StudioClock);
|
||||
|
||||
const ProtectedEditor = () => (
|
||||
<ProtectRoute>
|
||||
<Editor />
|
||||
</ProtectRoute>
|
||||
);
|
||||
|
||||
const ProtectedTable = () => (
|
||||
<ProtectRoute>
|
||||
<TableSettingsProvider>
|
||||
<Table />
|
||||
</TableSettingsProvider>
|
||||
</ProtectRoute>
|
||||
);
|
||||
|
||||
function App() {
|
||||
const { data } = useFetch(ALIASES, getAliases);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback((e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// check if the alt key is pressed
|
||||
if (e.altKey) {
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
// if we are in electron
|
||||
if (window.process?.type === 'renderer') {
|
||||
// ask to see debug
|
||||
window.ipcRenderer.send('set-window', 'show-dev');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
// navigate if is alias route
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
for (const d of data) {
|
||||
if (`/${d.alias}` === location.pathname && d.enabled) {
|
||||
navigate(`/${d.pathAndParams}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [data, location, navigate]);
|
||||
|
||||
return (
|
||||
<SocketProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className='App'>
|
||||
<Suspense fallback={null}>
|
||||
<Switch>
|
||||
<Route exact path='/' component={SSpeaker} />
|
||||
<Route exact path='/sm' component={SStageManager} />
|
||||
<Route exact path='/speaker' component={SSpeaker} />
|
||||
<Route exact path='/speakersimple' component={SSpeakerSimple} />
|
||||
<Route exact path='/editor' component={Editor} />
|
||||
<Route exact path='/public' component={SPublic} />
|
||||
<Route exact path='/pip' component={SPip} />
|
||||
{/* Lower cannot have fallback */}
|
||||
<Route exact path='/lower' component={SLowerThird} />
|
||||
{/* Send to default if nothing found */}
|
||||
<Route component={SSpeaker} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
</SocketProvider>
|
||||
<div className='App'>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<Routes>
|
||||
<Route path='/' element={<STimer />} />
|
||||
<Route path='/speaker' element={<STimer />} />
|
||||
<Route path='/presenter' element={<STimer />} />
|
||||
<Route path='/stage' element={<STimer />} />
|
||||
<Route path='/timer' element={<STimer />} />
|
||||
|
||||
<Route path='/minimal' element={<SMinimalTimer />} />
|
||||
<Route path='/minimalTimer' element={<SMinimalTimer />} />
|
||||
<Route path='/simpleTimer' element={<SMinimalTimer />} />
|
||||
|
||||
<Route path='/sm' element={<SStageManager />} />
|
||||
<Route path='/backstage' element={<SStageManager />} />
|
||||
|
||||
<Route path='/public' element={<SPublic />} />
|
||||
<Route path='/pip' element={<SPip />} />
|
||||
<Route path='/studio' element={<SStudio />} />
|
||||
{/*/!* Lower cannot have fallback *!/*/}
|
||||
<Route path='/lower' element={<SLowerThird />} />
|
||||
|
||||
{/*/!* Protected Routes *!/*/}
|
||||
<Route path='/editor' element={<ProtectedEditor />} />
|
||||
<Route path='/cuesheet' element={<ProtectedTable />} />
|
||||
<Route path='/cuelist' element={<ProtectedTable />} />
|
||||
<Route path='/table' element={<ProtectedTable />} />
|
||||
|
||||
{/* Send to default if nothing found */}
|
||||
<Route path='*' element={<STimer />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.15) rgba(255, 255, 255, 0.01);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
body,
|
||||
html,
|
||||
.App {
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
height: 100vh;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
option {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import {QueryClient} from "react-query";
|
||||
|
||||
export const queryClientMock = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
export const NODE_PORT = 4001;
|
||||
export const EVENT_TABLE = 'event';
|
||||
export const ALIASES = 'aliases';
|
||||
export const USERFIELDS = 'userFields';
|
||||
export const EVENTS_TABLE = 'events';
|
||||
export const APP_TABLE = 'appinfo';
|
||||
export const OSC_SETTINGS = 'oscSettings';
|
||||
export const APP_SETTINGS = 'appSettings';
|
||||
|
||||
const calculateServer = () => {
|
||||
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
|
||||
@@ -10,5 +14,5 @@ const calculateServer = () => {
|
||||
export const serverURL = calculateServer();
|
||||
export const eventURL = serverURL + EVENT_TABLE;
|
||||
export const eventsURL = serverURL + EVENTS_TABLE;
|
||||
export const playbackURL = serverURL + 'playback';
|
||||
export const ontimeURL = serverURL + 'ontime';
|
||||
export const playbackURL = `${serverURL}playback`;
|
||||
export const ontimeURL = `${serverURL}ontime`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { eventsURL } from '../api/apiConstants';
|
||||
import { eventsURL } from './apiConstants';
|
||||
|
||||
export const fetchAllEvents = async () => {
|
||||
const res = await axios.get(eventsURL);
|
||||
@@ -7,38 +7,30 @@ export const fetchAllEvents = async () => {
|
||||
};
|
||||
|
||||
export const requestPost = async (data) => {
|
||||
const res = await axios.post(eventsURL, data);
|
||||
return res;
|
||||
await axios.post(eventsURL, data);
|
||||
};
|
||||
|
||||
export const requestPut = async (data) => {
|
||||
const res = await axios.put(eventsURL, data);
|
||||
return res;
|
||||
await axios.put(eventsURL, data);
|
||||
};
|
||||
|
||||
export const requestPatch = async (data) => {
|
||||
const res = await axios.patch(eventsURL, data);
|
||||
return res;
|
||||
await axios.patch(eventsURL, data);
|
||||
};
|
||||
|
||||
export const requestReorder = async (data) => {
|
||||
const action = 'reorder';
|
||||
const res = await axios.patch(eventsURL + '/' + action, data);
|
||||
return res;
|
||||
await axios.patch(`${eventsURL}/reorder`, data);
|
||||
};
|
||||
|
||||
export const requestApplyDelay = async (eventId) => {
|
||||
const action = 'applydelay';
|
||||
const res = await axios.patch(eventsURL + '/' + action + '/' + eventId);
|
||||
return res;
|
||||
return await axios.patch(`${eventsURL}/${action}/${eventId}`);
|
||||
};
|
||||
|
||||
export const requestDelete = async (eventId) => {
|
||||
const res = await axios.delete(eventsURL + '/' + eventId);
|
||||
return res;
|
||||
await axios.delete(`${eventsURL}/${eventId}`);
|
||||
};
|
||||
|
||||
export const requestDeleteAll = async () => {
|
||||
const res = await axios.delete(eventsURL + '/all');
|
||||
return res;
|
||||
await axios.delete(`${eventsURL}/all`);
|
||||
};
|
||||
|
||||
@@ -3,21 +3,155 @@ import { ontimeURL } from './apiConstants';
|
||||
|
||||
export const ontimePlaceholderInfo = {
|
||||
networkInterfaces: [],
|
||||
settings: {
|
||||
version: '',
|
||||
serverPort: 4001,
|
||||
},
|
||||
};
|
||||
|
||||
export const ontimePlaceholderSettings = {
|
||||
pinCode: null,
|
||||
};
|
||||
|
||||
export const eventPlaceholderSettings = {
|
||||
title: '',
|
||||
url: '',
|
||||
publicInfo: '',
|
||||
backstageInfo: '',
|
||||
endMessage: '',
|
||||
};
|
||||
|
||||
export const userFieldsPlaceholder = {
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
};
|
||||
|
||||
export const oscPlaceholderSettings = {
|
||||
port: '',
|
||||
portOut: '',
|
||||
targetIP: '',
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
export const httpPlaceholder = {
|
||||
onLoad: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
export const getSettings = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/settings`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postSettings = async (data) => {
|
||||
await axios.post(`${ontimeURL}/settings`, data);
|
||||
};
|
||||
|
||||
export const getInfo = async () => {
|
||||
const res = await axios.get(ontimeURL + '/info');
|
||||
const res = await axios.get(`${ontimeURL}/info`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postInfo = async (data) => {
|
||||
await axios.post(`${ontimeURL}/info`, data);
|
||||
};
|
||||
|
||||
export const getAliases = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/aliases`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postAliases = async (data) => {
|
||||
await axios.post(`${ontimeURL}/aliases`, data);
|
||||
};
|
||||
|
||||
|
||||
export const getUserFields = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/userfields`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postUserFields = async (data) => {
|
||||
await axios.post(`${ontimeURL}/userfields`, data);
|
||||
};
|
||||
|
||||
export const getOSC = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/osc`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postOSC = async (data) => {
|
||||
await axios.post(`${ontimeURL}/osc`, data);
|
||||
};
|
||||
|
||||
export const downloadEvents = async () => {
|
||||
await axios({
|
||||
url: ontimeURL + '/db',
|
||||
url: `${ontimeURL}/db`,
|
||||
method: 'GET',
|
||||
responseType: 'blob', // important
|
||||
}).then((response) => {
|
||||
let headerLine = response.headers['Content-Disposition'];
|
||||
console.log(response);
|
||||
let filename = 'events.json';
|
||||
|
||||
// try and get the filename from the response
|
||||
@@ -38,17 +172,15 @@ export const downloadEvents = async () => {
|
||||
|
||||
export const uploadEvents = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('jsondb', file); // appending file
|
||||
formData.append('userFile', file); // appending file
|
||||
await axios
|
||||
.post(ontimeURL + '/db', formData, {
|
||||
.post(`${ontimeURL}/db`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then((res) => console.log(res.data))
|
||||
.catch((err) => console.error(err));
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadEventsWithPath = async (filepath) => {
|
||||
await axios.post(ontimeURL + '/dbpath', { path: filepath });
|
||||
await axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Exported viewer link location
|
||||
const speakerLocation = 'speaker';
|
||||
const smLocation = 'sm';
|
||||
const publicLocation = 'public';
|
||||
const pipLocation = 'pip';
|
||||
const studioLocation = 'studio';
|
||||
const cuesheetLocation = 'cuesheet';
|
||||
|
||||
export const viewerLocations = [
|
||||
{ link: speakerLocation, label: 'Speaker Screen' },
|
||||
{ link: smLocation, label: 'Backstage Screen' },
|
||||
{ link: publicLocation, label: 'Public Screen' },
|
||||
{ link: pipLocation, label: 'Picture in Picture' },
|
||||
{ link: studioLocation, label: 'Studio Clock' },
|
||||
{ link: cuesheetLocation, label: 'Cuesheet' }
|
||||
];
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useFetch } from '../hooks/useFetch';
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { getSettings } from '../api/ontimeApi';
|
||||
|
||||
export const AppContext = createContext({
|
||||
auth: false,
|
||||
data: {
|
||||
pinCode: null,
|
||||
},
|
||||
});
|
||||
|
||||
export const AppContextProvider = ({ children }) => {
|
||||
const [auth, setAuth] = useState(true);
|
||||
const { data } = useFetch(APP_SETTINGS, getSettings);
|
||||
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (data?.pinCode === null || data?.pinCode === '') {
|
||||
setAuth(true);
|
||||
} else {
|
||||
setAuth(false);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const validate = useCallback(
|
||||
(pin) => {
|
||||
const correct = pin === data.pinCode;
|
||||
setAuth(correct);
|
||||
return correct;
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
return <AppContext.Provider value={{ auth, validate }}>{children}</AppContext.Provider>;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { createContext, useCallback } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
export const CollapseContext = createContext({
|
||||
collapsed: {},
|
||||
setCollapsed: () => undefined,
|
||||
clearCollapsed: () => undefined,
|
||||
isCollapsed: () => undefined,
|
||||
});
|
||||
|
||||
export const CollapseProvider = ({ children }) => {
|
||||
const [collapsed, saveCollapsed] = useLocalStorage('collapsed', {});
|
||||
|
||||
/**
|
||||
* @description Sets collapsed state for a single id
|
||||
* @param {string} - id
|
||||
* @param {boolean} - collapsed / not collapsed
|
||||
*/
|
||||
const setCollapsed = useCallback(
|
||||
(id, isCollapsed) => {
|
||||
if (isCollapsed) {
|
||||
saveCollapsed((prev) => ({ ...prev, [id]: true }));
|
||||
} else {
|
||||
saveCollapsed((prev) => {
|
||||
const newObject = { ...prev };
|
||||
delete newObject[id];
|
||||
return { ...newObject };
|
||||
});
|
||||
}
|
||||
},
|
||||
[saveCollapsed]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Clears collapsed state in local-storage
|
||||
*/
|
||||
const collapseMultiple = useCallback(
|
||||
(events) => {
|
||||
const newOptions = {};
|
||||
for (const event of events) {
|
||||
newOptions[event.id] = true;
|
||||
}
|
||||
saveCollapsed(newOptions);
|
||||
},
|
||||
[saveCollapsed]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Clears collapsed state in local-storage
|
||||
*/
|
||||
const expandAll = useCallback(() => {
|
||||
saveCollapsed({});
|
||||
}, [saveCollapsed]);
|
||||
|
||||
/**
|
||||
* @description Clears collapsed state in local-storage
|
||||
* @return {boolean} - collapsed / not collapsed
|
||||
*/
|
||||
const isCollapsed = useCallback(
|
||||
(id) => {
|
||||
return id in collapsed;
|
||||
},
|
||||
[collapsed]
|
||||
);
|
||||
|
||||
return (
|
||||
<CollapseContext.Provider value={{ setCollapsed, collapseMultiple, expandAll, isCollapsed }}>
|
||||
{children}
|
||||
</CollapseContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
export const CursorContext = createContext({
|
||||
cursor: 0,
|
||||
isCursorLocked: false,
|
||||
|
||||
setCursor: () => undefined,
|
||||
moveCursorUp: () => undefined,
|
||||
moveCursorDown: () => undefined,
|
||||
});
|
||||
|
||||
export const CursorProvider = ({ children }) => {
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const [_cursorLocked, _setCursorLocked] = useLocalStorage('isCursorLocked', 'locked');
|
||||
const isCursorLocked = useMemo(() => _cursorLocked === 'locked', [_cursorLocked]);
|
||||
|
||||
const cursorLockedOff = useCallback(() => _setCursorLocked('unlocked'), [_setCursorLocked]);
|
||||
const cursorLockedOn = useCallback(() => _setCursorLocked('locked'), [_setCursorLocked]);
|
||||
|
||||
const moveCursorUp = useCallback(() => {
|
||||
setCursor((prev) => Math.max(prev - 1, 0));
|
||||
}, []);
|
||||
|
||||
const moveCursorDown = useCallback(() => {
|
||||
setCursor((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @param {boolean | undefined} newValue
|
||||
*/
|
||||
const toggleCursorLocked = useCallback(
|
||||
(newValue = undefined) => {
|
||||
if (newValue === undefined) {
|
||||
if (isCursorLocked) {
|
||||
cursorLockedOff();
|
||||
} else {
|
||||
cursorLockedOn();
|
||||
}
|
||||
} else if (!newValue) {
|
||||
cursorLockedOff();
|
||||
} else if (newValue) {
|
||||
cursorLockedOn();
|
||||
}
|
||||
},
|
||||
[cursorLockedOff, cursorLockedOn, isCursorLocked]
|
||||
);
|
||||
|
||||
return (
|
||||
<CursorContext.Provider
|
||||
value={{
|
||||
cursor,
|
||||
isCursorLocked,
|
||||
toggleCursorLocked,
|
||||
setCursor,
|
||||
moveCursorUp,
|
||||
moveCursorDown,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CursorContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { createContext, useState } from 'react';
|
||||
|
||||
export const LocalEventSettingsContext = createContext({
|
||||
showQuickEntry: false,
|
||||
starTimeIsLastEnd: true,
|
||||
defaultPublic: true,
|
||||
|
||||
setShowQuickEntry: () => undefined,
|
||||
setStarTimeIsLastEnd: () => undefined,
|
||||
setDefaultPublic: () => undefined,
|
||||
});
|
||||
|
||||
export const LocalEventSettingsProvider = ({ children }) => {
|
||||
const [showQuickEntry, setShowQuickEntry] = useState(false);
|
||||
const [starTimeIsLastEnd, setStarTimeIsLastEnd] = useState(true);
|
||||
const [defaultPublic, setDefaultPublic] = useState(false);
|
||||
|
||||
return (
|
||||
<LocalEventSettingsContext.Provider
|
||||
value={{
|
||||
showQuickEntry,
|
||||
setShowQuickEntry,
|
||||
starTimeIsLastEnd,
|
||||
setStarTimeIsLastEnd,
|
||||
defaultPublic,
|
||||
setDefaultPublic,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LocalEventSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useSocket } from './socketContext';
|
||||
import { generateId } from 'ontime-utils/generate_id';
|
||||
import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
|
||||
|
||||
export const LoggingContext = createContext({
|
||||
logData: [],
|
||||
emitInfo: () => undefined,
|
||||
emitWarning: () => undefined,
|
||||
emitError: () => undefined,
|
||||
clearLog: () => undefined,
|
||||
});
|
||||
|
||||
export const LoggingProvider = ({ children }) => {
|
||||
const MAX_MESSAGES = 100;
|
||||
const socket = useSocket();
|
||||
const [logData, setLogData] = useState([]);
|
||||
const origin = 'USER';
|
||||
|
||||
// handle incoming messages
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// Ask for log data
|
||||
socket.emit('get-logger');
|
||||
|
||||
socket.on('logger', (data) => {
|
||||
setLogData((l) => [data, ...l]);
|
||||
});
|
||||
|
||||
// Clear listener
|
||||
return () => {
|
||||
socket.off('logger');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
/**
|
||||
* Utility function sends message over socket
|
||||
* @param text
|
||||
* @param level
|
||||
* @private
|
||||
*/
|
||||
const _send = useCallback(
|
||||
(text, level) => {
|
||||
if (socket != null) {
|
||||
const m = {
|
||||
id: generateId(),
|
||||
origin,
|
||||
time: stringFromMillis(nowInMillis()),
|
||||
level,
|
||||
text,
|
||||
};
|
||||
setLogData((l) => [m, ...l]);
|
||||
socket.emit('logger', m);
|
||||
}
|
||||
if (logData.length > MAX_MESSAGES) {
|
||||
setLogData((l) => l.pop());
|
||||
}
|
||||
},
|
||||
[logData, socket]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level INFO
|
||||
* @param text
|
||||
*/
|
||||
const emitInfo = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'INFO');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level WARN
|
||||
* @param text
|
||||
*/
|
||||
const emitWarning = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'WARN');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level ERROR
|
||||
* @param text
|
||||
*/
|
||||
const emitError = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'ERROR');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Clears running log
|
||||
*/
|
||||
const clearLog = useCallback(() => {
|
||||
setLogData([]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoggingContext.Provider value={{ emitInfo, logData, emitWarning, emitError, clearLog }}>
|
||||
{children}
|
||||
</LoggingContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { createContext, useCallback, useState } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
export const TableSettingsContext = createContext({
|
||||
theme: '',
|
||||
showSettings: false,
|
||||
followSelected: false,
|
||||
|
||||
toggleSettings: () => undefined,
|
||||
toggleTheme: () => undefined,
|
||||
toggleFollow: () => undefined,
|
||||
});
|
||||
|
||||
export const TableSettingsProvider = ({ children }) => {
|
||||
const [theme, setTheme] = useLocalStorage('table-color-theme', 'dark');
|
||||
const [followSelected, setFollowSelected] = useLocalStorage('table-follow-selected', false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
/**
|
||||
* @description Toggles the current value of dark mode
|
||||
* @param {string} val - 'light' or 'dark'
|
||||
*/
|
||||
const toggleTheme = useCallback(
|
||||
(val) => {
|
||||
if (val === undefined) {
|
||||
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
|
||||
} else {
|
||||
setTheme(val);
|
||||
}
|
||||
},
|
||||
[setTheme]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Toggles visibility state for settings
|
||||
* @param {boolean} val - whether the settings window is visible
|
||||
*/
|
||||
const toggleSettings = useCallback(
|
||||
(val) => {
|
||||
if (val === undefined) {
|
||||
setShowSettings((prev) => !prev);
|
||||
} else {
|
||||
setShowSettings(val);
|
||||
}
|
||||
},
|
||||
[setShowSettings]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Toggles follow option
|
||||
* @param {boolean} val - whether the window follows selected event
|
||||
*/
|
||||
const toggleFollow = useCallback(
|
||||
(val) => {
|
||||
if (val === undefined) {
|
||||
setFollowSelected((prev) => !prev);
|
||||
} else {
|
||||
setFollowSelected(val);
|
||||
}
|
||||
},
|
||||
[setFollowSelected]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableSettingsContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
showSettings,
|
||||
followSelected,
|
||||
toggleSettings,
|
||||
toggleTheme,
|
||||
toggleFollow,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TableSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
const { atom } = require('jotai');
|
||||
|
||||
const PATH = 'option-collapse';
|
||||
const initialValue = {};
|
||||
|
||||
// collapse options object, initialised from local storage
|
||||
// REFRACT: When do we read from local storage?
|
||||
// on every render?
|
||||
|
||||
export const collapseAtom = atom(
|
||||
(get) => {
|
||||
const storedOptions = localStorage.getItem(PATH);
|
||||
if (storedOptions == null) return initialValue;
|
||||
|
||||
return JSON.parse(storedOptions);
|
||||
},
|
||||
(get, set, newValues) => {
|
||||
set(collapseAtom, newValues);
|
||||
localStorage.setItem(PATH, JSON.stringify(newValues));
|
||||
}
|
||||
);
|
||||
|
||||
// get a single option, if it exists
|
||||
export const SelectCollapse = (id) => {
|
||||
return atom((get) => get(collapseAtom)[id]);
|
||||
};
|
||||
|
||||
// change a single item in object
|
||||
export const HandleCollapse = atom(null, (get, set, payload) => {
|
||||
const updatedVal = {
|
||||
...get(collapseAtom),
|
||||
...payload,
|
||||
};
|
||||
set(collapseAtom, updatedVal);
|
||||
localStorage.setItem(PATH, JSON.stringify(updatedVal));
|
||||
});
|
||||
|
||||
// change collapsed in several items
|
||||
export const BatchOperation = atom(null, (get, set, payload) => {
|
||||
let prevOptions = get(collapseAtom);
|
||||
|
||||
let newOptions = {};
|
||||
|
||||
for (const item of payload.items) {
|
||||
newOptions[item.id] = payload.isCollapsed;
|
||||
}
|
||||
|
||||
if (payload.clear) {
|
||||
// clear object
|
||||
prevOptions = {};
|
||||
// clear localstorage
|
||||
localStorage.removeItem(PATH);
|
||||
}
|
||||
|
||||
const options = { ...prevOptions, ...newOptions };
|
||||
|
||||
set(collapseAtom, options);
|
||||
|
||||
localStorage.setItem(PATH, JSON.stringify(options));
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
const { atom } = require('jotai');
|
||||
|
||||
const PATH = 'option-gen';
|
||||
const initialValue = {
|
||||
cursor: 'locked',
|
||||
};
|
||||
|
||||
export const settingsAtom = atom(
|
||||
(get) => {
|
||||
const storedOptions = localStorage.getItem(PATH);
|
||||
if (storedOptions == null) return initialValue;
|
||||
|
||||
return JSON.parse(storedOptions);
|
||||
},
|
||||
(get, set, newValues) => {
|
||||
set(settingsAtom, newValues);
|
||||
localStorage.setItem(PATH, JSON.stringify(newValues));
|
||||
}
|
||||
);
|
||||
|
||||
// get a single option, if it exists
|
||||
export const SelectSetting = (setting) => {
|
||||
return atom((get) => get(settingsAtom)[setting]);
|
||||
};
|
||||
|
||||
// change a single item in object
|
||||
export const HandleOptions = atom(null, (get, set, payload) => {
|
||||
const updatedVal = {
|
||||
...get(settingsAtom),
|
||||
...payload,
|
||||
};
|
||||
set(settingsAtom, updatedVal);
|
||||
localStorage.setItem(PATH, JSON.stringify(updatedVal));
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import io from 'socket.io-client';
|
||||
import { serverURL } from 'app/api/apiConstants';
|
||||
|
||||
@@ -8,7 +8,7 @@ export const useSocket = () => {
|
||||
return useContext(SocketContext);
|
||||
};
|
||||
|
||||
function SocketProvider(props) {
|
||||
function SocketProvider({ children }) {
|
||||
const [socket, setSocket] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -17,10 +17,7 @@ function SocketProvider(props) {
|
||||
return () => s.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={socket}>
|
||||
{props.children}
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
|
||||
}
|
||||
|
||||
export default SocketProvider;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @description Validates two time entries
|
||||
* @param {number} timeStart
|
||||
* @param {number} timeEnd
|
||||
* @returns {{catch: string, value: boolean}}
|
||||
*/
|
||||
export const validateTimes = (timeStart, timeEnd) => {
|
||||
let validate = { value: true, catch: '' };
|
||||
if (timeStart > timeEnd) {
|
||||
validate.catch = 'Start time later than end time';
|
||||
}
|
||||
return validate;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
// Roughly from useHooks - useLocalStorage
|
||||
|
||||
export const useLocalStorage = (key, initialValue) => {
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(`ontime-${key}`);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
const setValue = (value) => {
|
||||
try {
|
||||
// Allow value to be a function so we have same API as useState
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value;
|
||||
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { EVENTS_TABLE } from '../api/apiConstants';
|
||||
|
||||
export default function useMutateEvents(mutation){
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(mutation, {
|
||||
onMutate: async (newEvent) => {
|
||||
// cancel ongoing queries
|
||||
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
||||
|
||||
// Return a context with the previous and new event
|
||||
return { previousEvent, newEvent };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
|
||||
},
|
||||
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: (newEvent) => {
|
||||
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
//
|
||||
// useTimeout React Hook
|
||||
//
|
||||
// React hook for delaying calls with time
|
||||
export const useTimeout = (
|
||||
callback, // function to call. No args passed.
|
||||
timeout = 0, // delay, ms (default: immediately put into JS Event Queue)
|
||||
{
|
||||
// manage re-render behavior.
|
||||
// by default, a re-render in your component will re-define the callback,
|
||||
// which will cause this timeout to cancel itself.
|
||||
// to avoid cancelling on re-renders (but still cancel on unmounts),
|
||||
// set `persistRenders: true,`.
|
||||
persistRenders = false,
|
||||
} = {},
|
||||
// These dependencies are injected for testing purposes.
|
||||
// (pure functions - where all dependencies are arguments - is often easier to test)
|
||||
_setTimeout = setTimeout,
|
||||
_clearTimeout = clearTimeout,
|
||||
_useEffect = useEffect
|
||||
) => {
|
||||
let timeoutId;
|
||||
const cancel = () => timeoutId && _clearTimeout(timeoutId);
|
||||
|
||||
_useEffect(
|
||||
() => {
|
||||
timeoutId = _setTimeout(callback, timeout);
|
||||
return cancel;
|
||||
},
|
||||
persistRenders
|
||||
? [_setTimeout, _clearTimeout]
|
||||
: [callback, timeout, _setTimeout, _clearTimeout]
|
||||
);
|
||||
|
||||
return cancel;
|
||||
};
|
||||
@@ -2,7 +2,7 @@ const dummy = new Date();
|
||||
|
||||
export const sampleData = {
|
||||
presenterMessage: {
|
||||
text: 'Only the presenter sees this',
|
||||
text: 'Only the timer sees this',
|
||||
active: false,
|
||||
},
|
||||
publicMessage: {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const userConfig = {
|
||||
timerColorOnPause: '#555',
|
||||
timerColorOnRunning: '#FFF',
|
||||
timerColorOnMessage: '#CCC',
|
||||
timerColorOnTimeOver: '#F00',
|
||||
overTimeText: '',
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export const clamp = (num, a, b) =>
|
||||
Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
||||
@@ -0,0 +1,27 @@
|
||||
import { validateAlias } from '../aliases';
|
||||
|
||||
describe('An alias fails if incorrect', () => {
|
||||
|
||||
const testsToFail = [
|
||||
// no empty
|
||||
'',
|
||||
// no https, http or www
|
||||
'https://www.test.com',
|
||||
'http://www.test.com',
|
||||
'www.test.com',
|
||||
// no hostname
|
||||
'localhost/test',
|
||||
'127.0.0.1/test',
|
||||
'0.0.0.0/test',
|
||||
// no editor
|
||||
'editor',
|
||||
'editor?test'
|
||||
];
|
||||
|
||||
testsToFail.forEach((t) => (
|
||||
test(`${t}`, () => {
|
||||
expect(validateAlias(t).status).toBeFalsy();
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { clamp } from '../math';
|
||||
|
||||
test('Clamps a set of numbers correctly', () => {
|
||||
const testCases = [
|
||||
{ num: 10, min: 0, max: 20, result: 10 },
|
||||
{ num: 0, min: 0, max: 20, result: 0 },
|
||||
{ num: 20, min: 0, max: 20, result: 20 },
|
||||
{ num: 20, min: 0, max: 20, result: 20 },
|
||||
{ num: -20, min: 0, max: 20, result: 0 },
|
||||
{ num: -0, min: 0, max: 20, result: 0 },
|
||||
{ num: -50, min: -30, max: -20, result: -30 },
|
||||
{ num: -50, min: 0, max: 0, result: 0 },
|
||||
{ num: 50.5, min: 0, max: 100, result: 50.5 },
|
||||
{ num: 50, min: 0, max: 20.32, result: 20.32 },
|
||||
{ num: 10, min: 20.32, max: 40, result: 20.32 }
|
||||
];
|
||||
|
||||
testCases.forEach((t) => (
|
||||
expect(clamp(t.num, t.min, t.max)).toBe(t.result)
|
||||
));
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Validates an alias against defined parameters
|
||||
* @param {string} alias
|
||||
* @returns {{message: string, status: boolean}}
|
||||
*/
|
||||
export const validateAlias = (alias) => {
|
||||
|
||||
const valid = { status: true, message: 'ok' };
|
||||
|
||||
if (alias === '' || alias == null) {
|
||||
// cannot be empty
|
||||
valid.status = false;
|
||||
valid.message = 'should not be empty';
|
||||
} else if (alias.includes('http') || alias.includes('https') || alias.includes('www')) {
|
||||
// cannot contain http, https or www
|
||||
valid.status = false;
|
||||
valid.message = 'should not include http, https, www';
|
||||
} else if (alias.includes('127.0.0.1') || alias.includes('localhost') || alias.includes('0.0.0.0')) {
|
||||
// aliases cannot contain hostname
|
||||
valid.status = false;
|
||||
valid.message = 'should not include hostname';
|
||||
} else if (alias.includes('editor')) {
|
||||
// no editor
|
||||
valid.status = false;
|
||||
valid.message = 'No aliases to editor page allowed';
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Clamps a value between a min and a max
|
||||
* @param {number} num - Value to clamp
|
||||
* @param {number} min - min value
|
||||
* @param {number} max - max value
|
||||
* @returns {number}
|
||||
*/
|
||||
export const clamp = (num, min, max) =>
|
||||
Math.max(Math.min(num, Math.max(min, max)), Math.min(min, max));
|
||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPlus } from 'react-icons/fi';
|
||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
|
||||
export default function AddIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
@@ -7,9 +7,11 @@ export default function AddIconBtn(props) {
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiPlus />}
|
||||
colorScheme='blue'
|
||||
onClick={clickhandler}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor={'orange.200'}
|
||||
color={'orange.500'}
|
||||
onClick={clickhandler}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsDown } from 'react-icons/fi';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
|
||||
export default function ApplyIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Apply delays'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
size={size || 'xs'}
|
||||
icon={<FiCheck />}
|
||||
colorScheme='orange'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiMinusCircle } from 'react-icons/fi';
|
||||
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||
|
||||
export default function BlockIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsUp } from 'react-icons/fi';
|
||||
import { FiChevronsUp } from '@react-icons/all-files/fi/FiChevronsUp';
|
||||
|
||||
export default function CollapseBtn(props) {
|
||||
const { clickhandler } = props;
|
||||
const { clickhandler, size } = props;
|
||||
return (
|
||||
<Tooltip label='Collapse all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiChevronsUp />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { FiTarget } from 'react-icons/fi';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
|
||||
export default function CurrentBtn(props) {
|
||||
const { clickhandler, active } = props;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
|
||||
|
||||
export default function CursorDownBtn(props) {
|
||||
const { clickhandler, active, ref, size } = props;
|
||||
return (
|
||||
<Tooltip label='Move cursor down Alt + ↓'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={size || 'xs'}
|
||||
icon={<IoCaretDown />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
backgroundColor={active ? 'pink.400' : undefined}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
+6
-5
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiTarget } from 'react-icons/fi';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
|
||||
export default function LockIconBtn(props) {
|
||||
const { clickhandler, active, ref } = props;
|
||||
export default function CursorLockedBtn(props) {
|
||||
const { clickhandler, active, ref, size } = props;
|
||||
return (
|
||||
<Tooltip label='Lock cursor to current'>
|
||||
<IconButton
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiTarget />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
|
||||
|
||||
export default function CursorUpBtn(props) {
|
||||
const { clickhandler, active, ref, size } = props;
|
||||
return (
|
||||
<Tooltip label='Move cursor up Alt + ↑'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={size || 'xs'}
|
||||
icon={<IoCaretUp />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
backgroundColor={active ? 'pink.400' : undefined}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock } from 'react-icons/fi';
|
||||
import { FiClock } from '@react-icons/all-files/fi/FiClock';
|
||||
|
||||
export default function DelayIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { useState } from 'react';
|
||||
import { FiMinus } from 'react-icons/fi';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function DeleteIconBtn(props) {
|
||||
const { actionHandler, ...rest } = props;
|
||||
const { actionHandler, size, ...rest } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -12,15 +14,17 @@ export default function DeleteIconBtn(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiMinus />}
|
||||
colorScheme='red'
|
||||
onClick={handleClick}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Delete'>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
onClick={handleClick}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
||||
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
|
||||
export default function EnableBtn(props) {
|
||||
const { active, text, actionHandler, size } = props;
|
||||
return (
|
||||
<Button
|
||||
size={size || 'xs'}
|
||||
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={actionHandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsDown } from 'react-icons/fi';
|
||||
import { FiChevronsDown } from '@react-icons/all-files/fi/FiChevronsDown';
|
||||
|
||||
export default function ExpandBtn(props) {
|
||||
const { clickhandler } = props;
|
||||
const { clickhandler, size } = props;
|
||||
return (
|
||||
<Tooltip label='Expand all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSkipForward } from 'react-icons/fi';
|
||||
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function NextIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiSkipForward />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipForward size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
|
||||
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function OnAirIconBtn(props) {
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={active ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPause } from 'react-icons/fi';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function PauseIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiPause />}
|
||||
colorScheme='orange'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPause size='24px' />}
|
||||
colorScheme='orange'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSkipBack } from 'react-icons/fi';
|
||||
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function PrevIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiSkipBack />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipBack size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiUsers } from 'react-icons/fi';
|
||||
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
||||
|
||||
export default function PublicIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiUsers />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiRefreshCcw } from 'react-icons/fi';
|
||||
import { IoArrowUndo } from '@react-icons/all-files/io5/IoArrowUndo';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function ReloadIconButton(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiRefreshCcw />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoArrowUndo size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock } from 'react-icons/fi';
|
||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function RollIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiClock />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoTimeOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPlay } from 'react-icons/fi';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function StartIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiPlay />}
|
||||
colorScheme='green'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlay size='24px' />}
|
||||
colorScheme='green'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiTrash2 } from 'react-icons/fi';
|
||||
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
|
||||
|
||||
export default function TrashIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiCornerLeftUp } from 'react-icons/fi';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function UnloadIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiCornerLeftUp />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoStop size='22px' />}
|
||||
colorScheme='red'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSun } from 'react-icons/fi';
|
||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function VisibleIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiSun />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label={active ? 'Make invisible' : 'Make visible'} openDelay={500}>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<IoSunny size='18px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import style from "../../../features/info/Info.module.scss";
|
||||
import {Icon} from "@chakra-ui/react";
|
||||
import {FiChevronUp} from "@react-icons/all-files/fi/FiChevronUp";
|
||||
|
||||
export default function CollapseBar(props) {
|
||||
const {title = 'Collapse bar', isCollapsed = false, onClick}= props;
|
||||
|
||||
return(
|
||||
<div className={style.header}>
|
||||
{title}
|
||||
<Icon
|
||||
className={isCollapsed ? style.moreCollapsed : style.moreExpanded}
|
||||
as={FiChevronUp}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
CollapseBar.propTypes = {
|
||||
title: PropTypes.string,
|
||||
isCollapsed: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.header,
|
||||
.header__roll {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: #ccc;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.header__roll {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import { memo } from 'react';
|
||||
import { formatDisplay } from 'common/dateConfig';
|
||||
import React, { memo } from 'react';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Countdown.module.css';
|
||||
|
||||
const Countdown = ({ time, small, negative, hideZeroHours }) => {
|
||||
let display = '-- : -- : --';
|
||||
|
||||
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
// prepare display string
|
||||
if (time != null && !isNaN(time))
|
||||
display = formatDisplay(Math.abs(time), hideZeroHours);
|
||||
const colour = negative ? '#ff7597' : '#fffffa';
|
||||
const display =
|
||||
time != null && !isNaN(time)
|
||||
? formatDisplay(time, hideZeroHours)
|
||||
: '-- : -- : --';
|
||||
|
||||
const colour = isNegative ? '#ff7597' : '#fffffa';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -21,3 +23,10 @@ const Countdown = ({ time, small, negative, hideZeroHours }) => {
|
||||
};
|
||||
|
||||
export default memo(Countdown);
|
||||
|
||||
Countdown.propTypes = {
|
||||
time: PropTypes.number,
|
||||
small: PropTypes.bool,
|
||||
isNegative: PropTypes.bool,
|
||||
hideZeroHours: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
static contextType = LoggingContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null, errorInfo: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// Update state so next render shows fallback UI.
|
||||
return { errorMessage: error.toString() };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: info,
|
||||
});
|
||||
this.context.emitError(error.toString());
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.errorMessage) {
|
||||
return <p>:/</p>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -1,24 +1,42 @@
|
||||
import React, { useContext } from 'react';
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { showWarningToast } from 'common/helpers/toastManager';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function EventTimes(props) {
|
||||
const { actionHandler, delay, timeStart, timeEnd } = props;
|
||||
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
const handleValidate = (entry, v) => {
|
||||
// we dont inforce validation here
|
||||
|
||||
if (v == null || timeStart == null || timeEnd == null) return true;
|
||||
/**
|
||||
* This code is duplicated from EventTimesVertical
|
||||
* @description Validates a time input against its pair
|
||||
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
|
||||
* @param {number} val - field value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const handleValidate = (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
|
||||
let validate = { value: true, catch: '' };
|
||||
if (entry === 'timeStart' && v > timeEnd)
|
||||
validate.catch = 'Start time later than end time';
|
||||
else if (entry === 'timeEnd' && v < timeStart)
|
||||
validate.catch = 'End time earlier than start time';
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
if (entry === 'timeStart') {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else if (entry === 'durationOverride'){
|
||||
return true;
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
if (validate.catch !== '')
|
||||
showWarningToast('Time Input Warning', validate.catch);
|
||||
return validate.value;
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return valid.value;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -29,6 +47,7 @@ export default function EventTimes(props) {
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
@@ -36,7 +55,16 @@ export default function EventTimes(props) {
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
EventTimes.propTypes = {
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React, { useContext } from 'react';
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { showWarningToast } from 'common/helpers/toastManager';
|
||||
import { stringFromMillis } from 'common/dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const label = {
|
||||
fontSize: '0.75em',
|
||||
@@ -8,8 +11,7 @@ const label = {
|
||||
};
|
||||
|
||||
const TimesDelayed = (props) => {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration } =
|
||||
props;
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
@@ -25,6 +27,7 @@ const TimesDelayed = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
@@ -35,6 +38,7 @@ const TimesDelayed = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
@@ -43,13 +47,24 @@ const TimesDelayed = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
const Times = (props) => {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration } = props;
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -60,6 +75,7 @@ const Times = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>End</span>
|
||||
<EditableTimer
|
||||
@@ -68,6 +84,7 @@ const Times = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
@@ -76,46 +93,83 @@ const Times = (props) => {
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function EventTimesVertical(props) {
|
||||
const { delay, timeStart, timeEnd, duration } = props;
|
||||
const handleValidate = (entry, v) => {
|
||||
// we dont enforce validation here
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
if (v == null || timeStart == null || timeEnd == null) return true;
|
||||
export default function EventTimesVertical(props) {
|
||||
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
/**
|
||||
* This code is duplicated from EventTimes
|
||||
* @description Validates a time input against its pair
|
||||
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
|
||||
* @param {number} val - field value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const handleValidate = (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
|
||||
let validate = { value: true, catch: '' };
|
||||
if (entry === 'timeStart' && v > timeEnd)
|
||||
validate.catch = 'Start time later than end time';
|
||||
else if (entry === 'timeEnd' && v < timeStart)
|
||||
validate.catch = 'End time earlier than start time';
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
if (entry === 'timeStart') {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else if (entry === 'durationOverride') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validate.catch !== '')
|
||||
showWarningToast('Time Input Warning', validate.catch);
|
||||
return validate.value;
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return valid.value;
|
||||
};
|
||||
|
||||
return (delay != null) & (delay > 0) ? (
|
||||
return delay != null && delay !== 0 ? (
|
||||
<TimesDelayed
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
actionHandler={actionHandler}
|
||||
delay={delay}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
) : (
|
||||
<Times
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
actionHandler={actionHandler}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
EventTimesVertical.propTypes = {
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { clamp } from 'app/utils';
|
||||
import React from 'react';
|
||||
import { clamp } from 'app/utils/math';
|
||||
import styles from './MyProgressBar.module.css';
|
||||
|
||||
export default function MyProgressBar(props) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Link, Redirect } from 'react-router-dom';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
import tinykeys from 'tinykeys';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import style from './NavLogo.module.css';
|
||||
import style from './NavLogo.module.scss';
|
||||
|
||||
export default function NavLogo() {
|
||||
export default function NavLogo(props) {
|
||||
const {isHidden} = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -14,20 +15,29 @@ export default function NavLogo() {
|
||||
};
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
let unsubscribe = tinykeys(window, {
|
||||
Space: () => {
|
||||
setShowNav((s) => !s);
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
const handleKeyPress = useCallback((e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Space bar
|
||||
if (e.keyCode === 32) {
|
||||
setShowNav((s) => !s);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
const baseOpacity = (isHidden) ? 0 : 0.5
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0.5 }}
|
||||
initial={{ opacity: baseOpacity }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
className={style.navContainer}
|
||||
>
|
||||
@@ -46,48 +56,61 @@ export default function NavLogo() {
|
||||
className={showNav ? style.nav : style.navHidden}
|
||||
>
|
||||
<Link
|
||||
to='/speaker'
|
||||
to='/timer'
|
||||
className={style.navItem}
|
||||
tabIndex={1}
|
||||
onKeyDownCapture={() => <Redirect push to='/speaker' />}
|
||||
>
|
||||
Speaker
|
||||
Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/minimal'
|
||||
className={style.navItem}
|
||||
tabIndex={2}
|
||||
>
|
||||
Minimal Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/sm'
|
||||
className={style.navItem}
|
||||
tabIndex={2}
|
||||
onKeyDownCapture={() => <Redirect push to='/sm' />}
|
||||
tabIndex={3}
|
||||
>
|
||||
Backstage
|
||||
</Link>
|
||||
<Link
|
||||
to='/public'
|
||||
className={style.navItem}
|
||||
tabIndex={3}
|
||||
onKeyDownCapture={() => <Redirect push to='/public' />}
|
||||
tabIndex={4}
|
||||
>
|
||||
Public
|
||||
</Link>
|
||||
<Link
|
||||
to='/lower'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
onKeyDownCapture={() => <Redirect push to='/lower' />}
|
||||
tabIndex={5}
|
||||
>
|
||||
Lower Thirds
|
||||
</Link>
|
||||
<Link
|
||||
to='/pip'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
onKeyDownCapture={() => <Redirect push to='/pip' />}
|
||||
tabIndex={6}
|
||||
>
|
||||
PIP
|
||||
</Link>
|
||||
<Link
|
||||
to='/studio'
|
||||
className={style.navItem}
|
||||
tabIndex={7}
|
||||
>
|
||||
Studio Clock
|
||||
</Link>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
NavLogo.propTypes = {
|
||||
isHidden: PropTypes.bool,
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,15 +1,18 @@
|
||||
$nav-color: #fff;
|
||||
|
||||
.navContainer {
|
||||
position: absolute;
|
||||
right: 2vw;
|
||||
top: 1vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-size: 1vw;
|
||||
font-size: max(1vw, 16px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 3vw;
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -23,4 +26,5 @@
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $nav-color;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './ProtectRoute.module.scss';
|
||||
import { PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import { AppContext } from '../../../app/context/AppContext';
|
||||
|
||||
export default function ProtectRoute({ children }) {
|
||||
const isLocal =
|
||||
window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||
const [pin, setPin] = useState('');
|
||||
const [failed, setFailed] = useState(false);
|
||||
const { auth, validate } = useContext(AppContext);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime';
|
||||
}, []);
|
||||
|
||||
const handleValidation = () => {
|
||||
const r = validate(pin);
|
||||
if (!r) {
|
||||
setFailed(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLocal && !auth ? (
|
||||
<div className={style.container}>
|
||||
ontime
|
||||
<div className={failed ? style.pin__failed : style.pin}>
|
||||
<PinInput
|
||||
type='alphanumeric'
|
||||
size='lg'
|
||||
mask
|
||||
onChange={(value) => {
|
||||
setFailed(false);
|
||||
setPin(value);
|
||||
}}
|
||||
>
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
</PinInput>
|
||||
<IconButton
|
||||
aria-label='Enter'
|
||||
size='lg'
|
||||
isRound
|
||||
icon={<FiCheck />}
|
||||
style={{ fontSize: '1.5em' }}
|
||||
onClick={() => handleValidation()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ProtectRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
@use '../../../styles/main' as *;
|
||||
@use '../../../styles/mixins' as *;
|
||||
|
||||
.container {
|
||||
background: #222;
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
height: 100vh;
|
||||
padding-bottom: 30vh;
|
||||
|
||||
color: $ontime-pink;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: 200;
|
||||
text-align: center;
|
||||
font-size: 3vw;
|
||||
}
|
||||
|
||||
.pin,
|
||||
.pin__failed {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
|
||||
input {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.pin__failed {
|
||||
input {
|
||||
animation: colourFade 1.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colourFade {
|
||||
from {
|
||||
background: $ontime-pink;
|
||||
}
|
||||
to {
|
||||
background: rgba($ontime-pink, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import styles from './SmallTimer.module.css';
|
||||
|
||||
export default function SmallTimer({ label, time }) {
|
||||
return (
|
||||
<div className={styles.SmallTimer}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.timer}>{time}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
.smallTimer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label,
|
||||
.timer {
|
||||
font-size: 1vw;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.5vw;
|
||||
color: #888;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
|
||||
font-size: 4vw;
|
||||
letter-spacing: 0.125em;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import TodayItem from './TodayItem';
|
||||
import style from './Paginator.module.css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useInterval } from 'app/hooks/useInterval';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function Paginator(props) {
|
||||
const { events, selectedId } = props;
|
||||
const LIMIT_PER_PAGE = props.limit || 8;
|
||||
const SCROLL_TIME = props.time * 1000 || 10000;
|
||||
const SCROLL_PAST = false;
|
||||
const { events, selectedId, limit = 7, time = 10, isBackstage } = props;
|
||||
const LIMIT_PER_PAGE = limit;
|
||||
const SCROLL_TIME = time * 1000 || 10000;
|
||||
const [numEvents, setNumEvents] = useState(0);
|
||||
const [page, setPage] = useState([]);
|
||||
const [pages, setPages] = useState(0);
|
||||
@@ -47,10 +47,7 @@ export default function Paginator(props) {
|
||||
<div className={style.nav}>
|
||||
{pages > 1 &&
|
||||
[...Array(pages)].map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === selPage ? style.navItemSelected : style.navItem}
|
||||
/>
|
||||
<div key={i} className={i === selPage ? style.navItemSelected : style.navItem} />
|
||||
))}
|
||||
</div>
|
||||
<div className={style.entries}>
|
||||
@@ -64,6 +61,7 @@ export default function Paginator(props) {
|
||||
timeStart={e.timeStart}
|
||||
timeEnd={e.timeEnd}
|
||||
title={e.title}
|
||||
colour={isBackstage ? e.colour : ''}
|
||||
backstageEvent={!e.isPublic}
|
||||
/>
|
||||
);
|
||||
@@ -72,3 +70,11 @@ export default function Paginator(props) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Paginator.propTypes = {
|
||||
events: PropTypes.array,
|
||||
selectedId: PropTypes.string,
|
||||
limit: PropTypes.number,
|
||||
time: PropTypes.number,
|
||||
isBackstage: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import style from './TitleCard.module.css';
|
||||
|
||||
export default function TitleCard(props) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import style from './TitleSide.module.css';
|
||||
|
||||
export default function TitleSide(props) {
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
import { stringFromMillis } from 'common/dateConfig';
|
||||
import React from 'react';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import style from './Paginator.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TodayItem(props) {
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent } = props;
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
|
||||
|
||||
// Format timers
|
||||
const start = stringFromMillis(timeStart, false) || '';
|
||||
const end = stringFromMillis(timeEnd, false) || '';
|
||||
|
||||
// user colours
|
||||
const userColour = colour !== '' ? colour : 'transparent';
|
||||
|
||||
// select styling
|
||||
let selectStyle = style.entryPast;
|
||||
if (selected === 1) selectStyle = style.entryNow;
|
||||
else if (selected === 2) selectStyle = style.entryFuture;
|
||||
return (
|
||||
<div className={selectStyle}>
|
||||
<div
|
||||
className={`${style.entryTimes} ${
|
||||
backstageEvent ? style.backstage : undefined
|
||||
}`}
|
||||
>{`${start} · ${end}`}</div>
|
||||
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : undefined}`}>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className={style.entryTitle}>{title}</div>
|
||||
{backstageEvent && <div className={style.backstageInd}></div>}
|
||||
{backstageEvent && <div className={style.backstageInd} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TodayItem.propTypes = {
|
||||
selected: PropTypes.bool,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
backstageEvent: PropTypes.bool,
|
||||
colour: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
export const timeFormat = 'HH:mm';
|
||||
export const timeFormatSeconds = 'HH:mm:ss';
|
||||
|
||||
// time string from miliseconds
|
||||
export const stringFromMillis = (
|
||||
ms,
|
||||
showSeconds = true,
|
||||
delim = ':',
|
||||
ifNull = '...'
|
||||
) => {
|
||||
if (ms === null || isNaN(ms)) return ifNull;
|
||||
const showWith0 = (value) => (value < 10 ? `0${value}` : value);
|
||||
const hours = showWith0(Math.floor(((ms / (1000 * 60 * 60)) % 60) % 24));
|
||||
const minutes = showWith0(Math.floor((ms / (1000 * 60)) % 60));
|
||||
const seconds = showWith0(Math.floor((ms / 1000) % 60));
|
||||
|
||||
return showSeconds
|
||||
? `${
|
||||
parseInt(hours) ? `${hours}${delim}` : `00${delim}`
|
||||
}${minutes}${delim}${seconds}`
|
||||
: `${parseInt(hours) ? `${hours}` : '00'}${delim}${minutes}`;
|
||||
};
|
||||
|
||||
// another go at simpler string formatting (counters)
|
||||
export function formatDisplay(seconds, hideZero) {
|
||||
const format = (val) => `0${Math.floor(val)}`.slice(-2);
|
||||
const hours = seconds / 3600;
|
||||
const minutes = (seconds % 3600) / 60;
|
||||
|
||||
if (hideZero && hours < 1)
|
||||
return [minutes, seconds % 60].map(format).join(':');
|
||||
else return [hours, minutes, seconds % 60].map(format).join(':');
|
||||
}
|
||||
|
||||
// millis to seconds
|
||||
export const millisToSeconds = (millis) => {
|
||||
return Math.floor(millis / 1000);
|
||||
};
|
||||
|
||||
// millis to minutes
|
||||
export const millisToMinutes = (millis) => {
|
||||
return Math.floor(millis / 60000);
|
||||
};
|
||||
|
||||
// timeStringToMillis
|
||||
export const timeStringToMillis = (string) => {
|
||||
let time = string.split(':');
|
||||
return time[0] * 3600000 + time[1] * 60000;
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import { createStandaloneToast } from '@chakra-ui/react';
|
||||
|
||||
const toast = createStandaloneToast();
|
||||
// const customToast = createStandaloneToast({ theme: yourCustomTheme })
|
||||
|
||||
// error toast
|
||||
export const showErrorToast = (title, description) => {
|
||||
toast({
|
||||
title: title,
|
||||
description: description,
|
||||
position: 'top-left',
|
||||
variant: 'subtle',
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
|
||||
// warning toast
|
||||
export const showWarningToast = (title, description) => {
|
||||
toast({
|
||||
title: title,
|
||||
description: description,
|
||||
position: 'top-left',
|
||||
variant: 'subtle',
|
||||
status: 'warning',
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Textarea } from '@chakra-ui/react';
|
||||
import autosize from 'autosize/dist/autosize';
|
||||
|
||||
export const AutoTextArea = (props) => {
|
||||
const ref = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const node = ref.current;
|
||||
autosize(ref.current);
|
||||
return () => {
|
||||
autosize.destroy(node);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
overflow='hidden'
|
||||
w='100%'
|
||||
resize='none'
|
||||
ref={ref}
|
||||
transition='height none'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { FormErrorMessage } from '@chakra-ui/form-control';
|
||||
import { FormLabel } from '@chakra-ui/form-control';
|
||||
import { FormControl } from '@chakra-ui/form-control';
|
||||
import { Input } from '@chakra-ui/input';
|
||||
import { Field } from 'formik';
|
||||
|
||||
export default function ChakraInput(props) {
|
||||
const { label, name, ...rest } = props;
|
||||
return (
|
||||
<Field name={name}>
|
||||
{({ field, form }) => {
|
||||
return (
|
||||
<FormControl isInvalid={form.errors[name] && form.touched[name]}>
|
||||
<FormLabel htmlFor={name}>{label}</FormLabel>
|
||||
<Input id={name} {...rest} {...field} />
|
||||
<FormErrorMessage>{form.errors[name]}</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { clamp } from '../../app/utils/math';
|
||||
import style from './TimeInput.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const inputProps = {
|
||||
width: 20,
|
||||
@@ -8,46 +10,48 @@ const inputProps = {
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
color: '#fff',
|
||||
border: '1px solid #ecc94b55',
|
||||
borderRadius: '4px',
|
||||
borderRadius: '8px',
|
||||
placeholder: '-',
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
export default function DelayInput(props) {
|
||||
const [value, setValue] = useState(props.value);
|
||||
const { actionHandler, value } = props;
|
||||
const [_value, setValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value == null) return;
|
||||
setValue(props.value);
|
||||
}, [props.value]);
|
||||
|
||||
const handleChange = (val) => {
|
||||
if (val <= 666) setValue(val);
|
||||
};
|
||||
if (value == null) return;
|
||||
setValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleSubmit = (val) => {
|
||||
if (val === props.value) return;
|
||||
if (val === value) return;
|
||||
if (val === '') setValue(0);
|
||||
if (val >= 0 && val <= 60) {
|
||||
// convert to ms and updates
|
||||
props.actionHandler('update', { field: 'duration', value: val * 60000 });
|
||||
} else {
|
||||
props.actionHandler('update', { field: 'duration', value: 3600000 });
|
||||
}
|
||||
|
||||
// convert to ms and updates
|
||||
const msVal = clamp(val, -60, 60) * 60000;
|
||||
actionHandler('update', { field: 'duration', value: msVal });
|
||||
};
|
||||
|
||||
const labelText = `minutes ${value >= 0 ? 'delayed' : 'ahead'}`;
|
||||
|
||||
return (
|
||||
<div className={style.timeInput}>
|
||||
<Editable
|
||||
{...inputProps}
|
||||
value={value}
|
||||
onChange={(v) => handleChange(v)}
|
||||
value={_value}
|
||||
onChange={(v) => setValue(v)}
|
||||
onSubmit={(v) => handleSubmit(v)}
|
||||
>
|
||||
<EditablePreview />
|
||||
<EditableInput type='number' min='0' max='60' />
|
||||
<EditableInput type='number' min='-60' max='60' />
|
||||
</Editable>
|
||||
<span className={style.label}>{'minutes'}</span>
|
||||
<span className={style.label}>{labelText}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DelayInput.propTypes = {
|
||||
actionHandler: PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import style from './EditableText.module.css';
|
||||
import style from './EditableText.module.scss';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function EditableText(props) {
|
||||
const { label, defaultValue, placeholder, submitHandler, ...rest } = props;
|
||||
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
|
||||
const [text, setText] = useState(defaultValue || '');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -11,14 +12,20 @@ export default function EditableText(props) {
|
||||
else setText(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
const handleSubmit = (submitedVal) => {
|
||||
const handleSubmit = (submittedVal) => {
|
||||
// No need to update if it hasnt changed
|
||||
if (submitedVal === defaultValue) return;
|
||||
submitHandler(submitedVal);
|
||||
if (submittedVal === defaultValue) return;
|
||||
// submit a cleaned up version of the string
|
||||
const cleanVal = submittedVal.trim();
|
||||
submitHandler(cleanVal);
|
||||
|
||||
if (cleanVal !== submittedVal) {
|
||||
setText(cleanVal);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (val) => {
|
||||
if (val.length < 40) setText(val);
|
||||
if (val.length < maxchar) setText(val);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -32,12 +39,17 @@ export default function EditableText(props) {
|
||||
className={style.inline}
|
||||
{...rest}
|
||||
>
|
||||
<EditablePreview
|
||||
color={text === '' ? '#666' : 'inherit'}
|
||||
maxWidth='75%'
|
||||
/>
|
||||
<EditableInput overflowX='hidden' maxWidth='75%' />
|
||||
<EditablePreview className={text === '' ? style.preview : ''} />
|
||||
<EditableInput />
|
||||
</Editable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EditableText.propTypes = {
|
||||
label: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
submitHandler: PropTypes.func.isRequired,
|
||||
maxchar: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.title {
|
||||
padding-left: 1em;
|
||||
font-size: 0.75em;
|
||||
color: #aaa;
|
||||
display: inline-block;
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: 'block';
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@use '../../styles/main' as *;
|
||||
|
||||
.block {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
padding-left: 1em;
|
||||
font-size: 0.75em;
|
||||
color: $label-gray;
|
||||
display: inline-block;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.preview {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,94 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
timeFormat,
|
||||
stringFromMillis,
|
||||
timeStringToMillis,
|
||||
} from '../dateConfig';
|
||||
import { showErrorToast } from '../helpers/toastManager';
|
||||
import { forgivingStringToMillis } from '../utils/dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import style from './EditableTimer.module.css';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function EditableTimer(props) {
|
||||
const { name, actionHandler, time, delay, validate } = props;
|
||||
const { name, actionHandler, time, delay, validate, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
// prepare time fields
|
||||
const validateValue = (value) => {
|
||||
const success = handleSubmit(value);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(value);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay, false));
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
showErrorToast('Error parsing date', error.text);
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [time, delay]);
|
||||
|
||||
const validateValue = (value) => {
|
||||
const success = handleSubmit(value);
|
||||
|
||||
if (success) setValue(value);
|
||||
else setValue(stringFromMillis(time + delay, false));
|
||||
};
|
||||
}, [time, delay, emitError]);
|
||||
|
||||
const handleSubmit = (value) => {
|
||||
// Check if there is anything there
|
||||
if (value === '') return false;
|
||||
|
||||
// Time now and time submitedVal
|
||||
const original = stringFromMillis(time + delay, false);
|
||||
let newValMillis = 0;
|
||||
|
||||
// check for known aliases
|
||||
if (value === 'p' || value === 'prev' || value === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
}
|
||||
} else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) {
|
||||
// string to pass should add to the end before
|
||||
const val = value.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(value);
|
||||
}
|
||||
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
|
||||
// check if time is different from before
|
||||
if (value === original) return false;
|
||||
|
||||
// convert to millis object
|
||||
const millis = timeStringToMillis(value, timeFormat);
|
||||
if (newValMillis === originalMillis) return false;
|
||||
|
||||
// validate with parent
|
||||
if (!validate(name, millis)) return false;
|
||||
if (!validate(name, newValMillis)) return false;
|
||||
|
||||
// update entry
|
||||
actionHandler('update', { field: name, value: millis });
|
||||
actionHandler('update', { field: name, value: newValMillis });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const isDelayed = delay != null && delay !== 0;
|
||||
|
||||
return (
|
||||
<Editable
|
||||
data-testid='editable-timer'
|
||||
onChange={(v) => setValue(v)}
|
||||
onSubmit={(v) => validateValue(v)}
|
||||
onCancel={() => setValue(stringFromMillis(time + delay, true))}
|
||||
value={value}
|
||||
placeholder='--:--'
|
||||
className={delay > 0 ? style.delayedEditable : style.editable}
|
||||
className={isDelayed ? style.delayedEditable : style.editable}
|
||||
>
|
||||
<EditablePreview />
|
||||
<EditableInput type='time' min='00:00' max='23:59' />
|
||||
<EditableInput type='text' placeholder='--:--:--' data-testid='editable-timer-input' />
|
||||
</Editable>
|
||||
);
|
||||
}
|
||||
|
||||
EditableTimer.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
time: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
validate: PropTypes.func.isRequired,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
.editable,
|
||||
.delayedEditable {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
width: 5em;
|
||||
width: 6.5em;
|
||||
letter-spacing: 1px;
|
||||
height: fit-content;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.delayedEditable {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import EditableTimer from '../EditableTimer';
|
||||
|
||||
const renderEditableTimer = (
|
||||
name = 'test',
|
||||
actionHandler = () => undefined,
|
||||
validate = () => undefined
|
||||
) => render(<EditableTimer name={name} actionHandler={actionHandler} validate={validate} />);
|
||||
|
||||
describe('test EditableTimer component', () => {
|
||||
const testName = "test";
|
||||
const actionHandler = jest.fn();
|
||||
const validate = jest.fn();
|
||||
|
||||
renderEditableTimer(testName, actionHandler, validate);
|
||||
const editableTimer = screen.getByTestId('editable-timer');
|
||||
const editableInput = screen.getByTestId('editable-timer-input');
|
||||
|
||||
it('renders correctly', () => {
|
||||
expect(editableTimer).toBeInTheDocument();
|
||||
expect(editableInput).toBeInTheDocument();
|
||||
|
||||
userEvent.type(editableInput, 'p');
|
||||
expect(editableInput).toHaveValue('p');
|
||||
|
||||
userEvent.type(editableInput, '{enter}');
|
||||
|
||||
// no previous is given, defaults to 0
|
||||
expect(validate).toHaveBeenCalledWith(testName, 0);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
||||
import style from './Empty.module.css';
|
||||
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
import {
|
||||
forgivingStringToMillis,
|
||||
formatDisplay,
|
||||
isTimeString,
|
||||
millisToMinutes,
|
||||
millisToSeconds,
|
||||
timeStringToMillis,
|
||||
} from '../dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
|
||||
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 valid millis', () => {
|
||||
const t = { val: 3600, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600, 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: 86400, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401, result: '00:00:01' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401, result: '00:00:01' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test formatDisplay handles partial secs', () => {
|
||||
it('test with 1795829', () => {
|
||||
const t = { val: 1795829, result: '00:29:55' };
|
||||
expect(stringFromMillis(t.val)).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: 3600, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600, 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: 86400, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401, result: '00:01' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401, result: '00:01' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test millisToSeconds function', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: 0 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: 3600 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: -3600 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: 0 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: -0 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401000 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: 86401 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401000 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: -86401 };
|
||||
expect(millisToSeconds(t.val, false)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test millisToMinutes function', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: 0 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: 60 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: -60 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: 0 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: -0 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401000 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: 1440 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401000 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: -1440 };
|
||||
expect(millisToMinutes(t.val, false)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test timeStringToMillis function', () => {
|
||||
it('test with null', () => {
|
||||
const t = { val: null, result: 0 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 00:00:00', () => {
|
||||
const t = { val: '00:00:00', result: 0 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -00:00:00', () => {
|
||||
const t = { val: '-00:00:00', result: 0 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 00:00:01', () => {
|
||||
const t = { val: '00:00:01', result: 1000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -00:00:01', () => {
|
||||
const t = { val: '-00:00:01', result: 1000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 01:00:01', () => {
|
||||
const t = { val: '01:00:01', result: 3601000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 24:00:01', () => {
|
||||
const t = { val: '24:00:01', result: 86401000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 00:00:5', () => {
|
||||
const t = { val: '00:00:5', result: 5000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 00:1:00', () => {
|
||||
const t = { val: '00:1:00', result: 60000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 1:00:00', () => {
|
||||
const t = { val: '1:00:00', result: 3600000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 1', () => {
|
||||
const t = { val: '1', result: 1000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 120', () => {
|
||||
const t = { val: '120', result: 120000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 56', () => {
|
||||
const t = { val: '56', result: 56000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 2:3', () => {
|
||||
const t = { val: '2:3', result: 123000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 02:3', () => {
|
||||
const t = { val: '02:3', result: 123000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 2:03', () => {
|
||||
const t = { val: '2:03', result: 123000 };
|
||||
expect(timeStringToMillis(t.val)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test isTimeString() function', () => {
|
||||
test('it validates time strings', () => {
|
||||
const ts = ['2', '2:10', '2:10:22'];
|
||||
for (const s of ts) {
|
||||
expect(isTimeString(s)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('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) {
|
||||
test(`it handles ${s}`, () => {
|
||||
expect(isTimeString(s)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('test forgivingStringToMillis()', () => {
|
||||
describe('function handles separators', () => {
|
||||
const testData = [
|
||||
{ value: '1:2:3:10', expect: 3723000 },
|
||||
{ value: '2,10', expect: 130000 },
|
||||
{ value: '2.10', expect: 130000 },
|
||||
{ value: '2 10', expect: 130000 },
|
||||
];
|
||||
|
||||
for (const s of testData) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('function handles time with no separators', () => {
|
||||
const testData = [
|
||||
{ value: '000000', expect: 0 },
|
||||
{ value: '000001', expect: 1000 },
|
||||
{ value: '000100', expect: 1000*60 },
|
||||
{ value: '010000', expect: 1000*60*60 },
|
||||
{ value: '230000', expect: 1000*60*60*23 },
|
||||
{ value: '121212', expect: 12*1000+12*60*1000+12*1000*60*60 },
|
||||
];
|
||||
|
||||
for (const s of testData) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('parses strings correctly', () => {
|
||||
const ts = [
|
||||
{ value: '', expect: 0 },
|
||||
{ value: '0', expect: 0 },
|
||||
{ value: '-0', expect: 0 },
|
||||
{ value: '1', expect: 60 * 1000 },
|
||||
{ value: '-1', expect: 60 * 1000 },
|
||||
{ value: '1.2', expect: 60 * 1000 + 2 * 1000 },
|
||||
{ value: '1.70', expect: 60 * 1000 + 70 * 1000 },
|
||||
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
|
||||
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('handles overflows', () => {
|
||||
const ts = [
|
||||
// minutes overflow
|
||||
{ value: '120', expect: 1000*60*120 },
|
||||
{ value: '2.0.0', expect: 1000*60*120 },
|
||||
{ value: '99', expect: 1000*60*99 },
|
||||
{ value: '1.39.0', expect: 1000*60*99 },
|
||||
// seconds overflow
|
||||
{ value: '0.120', expect: 120*1000 },
|
||||
{ value: '0.0.120', expect: 120*1000 },
|
||||
{ value: '0.2.0', expect: 120*1000 },
|
||||
{ value: '0.99', expect: 99*1000 },
|
||||
{ value: '0.0.99', expect: 99*1000 },
|
||||
{ value: '0.1.39', expect: 99*1000 },
|
||||
// hours overflow
|
||||
{ value: '25.0.0', expect: 1000*60*60*25 },
|
||||
// hours overflow
|
||||
{ value: '50.0.0', expect: 1000*60*60*50 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user