mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-03 06:28:01 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f68b5f1d74 | |||
| db0eee3b9c | |||
| 20d3ddbc18 | |||
| 82d0508672 | |||
| dd1b9bd38c | |||
| b01145ca4b | |||
| 67ddcd8c68 | |||
| 8841a02754 | |||
| 1a0d7810c4 | |||
| f2dd24b74a | |||
| 6a6f68b2f9 | |||
| 906a6631a7 |
@@ -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"]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,141 @@
|
||||
name: Ontime build
|
||||
|
||||
on:
|
||||
push:
|
||||
# run when a tag is created
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_mac:
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-mac
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-macOS.dmg
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build_win:
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-mac
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-win64.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# build_linux:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# CI: ''
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Use Node.js
|
||||
# uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: '14.x'
|
||||
#
|
||||
# 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 - Build project
|
||||
# run: yarn build
|
||||
# working-directory: ./client
|
||||
#
|
||||
# Node server
|
||||
# - name: Server - Install dependencies
|
||||
# run: yarn install
|
||||
# working-directory: ./server/src
|
||||
#
|
||||
# App
|
||||
# - name: Electron - Install dependencies
|
||||
# run: yarn install && yarn make && yarn setdb
|
||||
# working-directory: ./server
|
||||
# - name: Electron - Build app
|
||||
# run: yarn dist-linux
|
||||
# working-directory: ./server
|
||||
#
|
||||
# Release
|
||||
# - name: Release
|
||||
# uses: softprops/action-gh-release@v1
|
||||
# with:
|
||||
# files: ./server/dist/ontime-win64.exe
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -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
|
||||
@@ -19,20 +19,7 @@ jobs:
|
||||
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
|
||||
@@ -46,17 +33,13 @@ jobs:
|
||||
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
|
||||
run: yarn install && yarn setdb
|
||||
working-directory: ./server
|
||||
|
||||
- name: Electron - Run tests
|
||||
|
||||
+13
@@ -1,6 +1,19 @@
|
||||
<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">
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
|
||||
Download the latest releases here
|
||||
- [Windows](https://gitreleases.dev/gh/cpvalente/ontime/latest/ontime-win64.exe)
|
||||
<div style="display: flex; justify-content: space-around">
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/mac-download.png"/></a>
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/win-download.png"/></a>
|
||||
<img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/>
|
||||
</div>
|
||||
|
||||
# Ontime
|
||||
Ontime is an application for managing event rundowns and running stage timers.
|
||||
@@ -26,17 +30,18 @@ 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/preseter > 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 > Web server default to presenter timer view
|
||||
IP.ADDRESS:4001/timer > 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
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
|
||||
```
|
||||
|
||||
@@ -48,7 +53,9 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
- 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
|
||||
@@ -62,7 +69,6 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
## Unopinionated
|
||||
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] Don't have or care for a schedule?
|
||||
@@ -83,25 +89,44 @@ Taking advantage of the integrations in Ontime, we currently use Ontime with:
|
||||
### 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.
|
||||
- [ ] HTTP Server (vMix integration)
|
||||
- [ ] Improvement with event component design
|
||||
- [ ] New playback mode for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
|
||||
- [ ] 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
|
||||
|
||||
### 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
|
||||
The app is still in pre-release and there are a few issues, mainly concerning responsiveness in
|
||||
different screens. If you run into problems, please open an issue with a screenshot and your device
|
||||
resolution
|
||||
|
||||
#### 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
|
||||
#### Unsigned App
|
||||
|
||||
When installing the app you would see warning screens from the Operating System like:
|
||||
|
||||
```Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.```
|
||||
or
|
||||
```Ontime can't be opened because it is from an unidentified developer```
|
||||
Long story short: Ontime app is unsigned. </br>Purchasing the certificates for both Mac and Windows
|
||||
incurs an early cost and is not a priority. This is unlikely to change in future.
|
||||
If you have tips on how to improve this, or would like to sponsor the code signing,
|
||||
please [open an issue so we can discuss it](https://github.com/cpvalente/ontime/issues/new)
|
||||
|
||||
#### Safari
|
||||
|
||||
There are some issues with Safari versions lower than 13:
|
||||
- Spacing and text styles do not render correctly
|
||||
- Table view does not work
|
||||
|
||||
There is no plan for any further work on this since the breaking code belongs to third party libraries.
|
||||
|
||||
# Help
|
||||
Help is underway! ... and can be viewed [here](https://cpvalente.gitbook.io/ontime/)
|
||||
|
||||
+73
-4
@@ -1,13 +1,82 @@
|
||||
{
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
"react-app/jest",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"testing-library",
|
||||
"jest"
|
||||
],
|
||||
"plugins": ["react", "testing-library", "jest"],
|
||||
"rules": {
|
||||
"jest/no-mocks-import": "warn",
|
||||
"no-useless-concat": "warn",
|
||||
"prefer-template": "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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+18
-14
@@ -3,29 +3,29 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.7.3",
|
||||
"@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",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"axios": "^0.24.0",
|
||||
"framer-motion": "^4.1.6",
|
||||
"jotai": "^0.16.5",
|
||||
"ontime-utils": "link: ../server/utils/",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"autosize": "^5.0.1",
|
||||
"axios": "^0.25.0",
|
||||
"color": "^4.2.1",
|
||||
"framer-motion": "4.1.17",
|
||||
"react": "17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-qr-code": "2.0.3",
|
||||
"react-query": "^3.34.5",
|
||||
"react-query": "^3.34.12",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-scripts": "5.0.0",
|
||||
"socket.io-client": "4.4.0",
|
||||
"react-table": "^7.7.0",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"typeface-open-sans": "^1.1.13",
|
||||
"use-fit-text": "^2.4.0",
|
||||
"web-vitals": "^1.0.1"
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "set BROWSER=none&&react-scripts start",
|
||||
@@ -47,9 +47,13 @@
|
||||
]
|
||||
},
|
||||
"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.44.0"
|
||||
"sass": "^1.49.0"
|
||||
}
|
||||
}
|
||||
|
||||
+28
-23
@@ -1,4 +1,4 @@
|
||||
import { lazy, Suspense, useCallback, useEffect } from 'react';
|
||||
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';
|
||||
@@ -7,23 +7,17 @@ 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 Table = lazy(() => import('features/table/TableWrapper'));
|
||||
|
||||
const TimerView = lazy(() =>
|
||||
import('features/viewers/timer/Timer')
|
||||
);
|
||||
const MinimalTimerView = lazy(() =>
|
||||
import('features/viewers/timer/MinimalTimer')
|
||||
);
|
||||
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 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'));
|
||||
|
||||
@@ -35,6 +29,20 @@ 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();
|
||||
@@ -48,8 +56,7 @@ function App() {
|
||||
if (e.altKey) {
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
// if we are in electron
|
||||
if (window.process?.type === undefined) return;
|
||||
if (window.process.type === 'renderer') {
|
||||
if (window.process?.type === 'renderer') {
|
||||
// ask to see debug
|
||||
window.ipcRenderer.send('set-window', 'show-dev');
|
||||
}
|
||||
@@ -101,15 +108,13 @@ function App() {
|
||||
<Route path='/studio' element={<SStudio />} />
|
||||
{/*/!* Lower cannot have fallback *!/*/}
|
||||
<Route path='/lower' element={<SLowerThird />} />
|
||||
|
||||
{/*/!* Protected Routes *!/*/}
|
||||
<Route
|
||||
path='/editor'
|
||||
element={
|
||||
<ProtectRoute>
|
||||
<Editor />
|
||||
</ProtectRoute>
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
|
||||
+3
-2
@@ -8,7 +8,7 @@
|
||||
body,
|
||||
html,
|
||||
.App {
|
||||
margin: 0px auto;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
height: 100vh;
|
||||
@@ -23,7 +23,8 @@ option {
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -13,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`;
|
||||
|
||||
@@ -6,7 +6,4 @@ export const fetchEvent = async () => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postEvent = async (data) => {
|
||||
const res = await axios.post(eventURL, data);
|
||||
return res;
|
||||
};
|
||||
export const postEvent = async (data) => axios.post(eventURL, data);
|
||||
|
||||
@@ -6,32 +6,16 @@ export const fetchAllEvents = async () => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const requestPost = async (data) => {
|
||||
return await axios.post(eventsURL, data);
|
||||
};
|
||||
export const requestPost = async (data) => axios.post(eventsURL, data);
|
||||
|
||||
export const requestPut = async (data) => {
|
||||
return await axios.put(eventsURL, data);
|
||||
};
|
||||
export const requestPut = async (data) => axios.put(eventsURL, data);
|
||||
|
||||
export const requestPatch = async (data) => {
|
||||
return await axios.patch(eventsURL, data);
|
||||
};
|
||||
export const requestPatch = async (data) => axios.patch(eventsURL, data);
|
||||
|
||||
export const requestReorder = async (data) => {
|
||||
const action = 'reorder';
|
||||
return await axios.patch(eventsURL + '/' + action, data);
|
||||
};
|
||||
export const requestReorder = async (data) => axios.patch(`${eventsURL}/reorder`, data);
|
||||
|
||||
export const requestApplyDelay = async (eventId) => {
|
||||
const action = 'applydelay';
|
||||
return await axios.patch(eventsURL + '/' + action + '/' + eventId);
|
||||
};
|
||||
export const requestApplyDelay = async (eventId) => axios.patch(`${eventsURL}/applydelay/${eventId}`);
|
||||
|
||||
export const requestDelete = async (eventId) => {
|
||||
return await axios.delete(eventsURL + '/' + eventId);
|
||||
};
|
||||
export const requestDelete = async (eventId) => axios.delete(`${eventsURL}/${eventId}`);
|
||||
|
||||
export const requestDeleteAll = async () => {
|
||||
return await axios.delete(eventsURL + '/all');
|
||||
};
|
||||
export const requestDeleteAll = async () => axios.delete(`${eventsURL}/all`);
|
||||
|
||||
@@ -21,6 +21,19 @@ export const eventPlaceholderSettings = {
|
||||
endMessage: '',
|
||||
};
|
||||
|
||||
export const userFieldsPlaceholder = {
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
};
|
||||
|
||||
export const oscPlaceholderSettings = {
|
||||
port: '',
|
||||
portOut: '',
|
||||
@@ -91,36 +104,35 @@ export const getSettings = async () => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postSettings = async (data) => {
|
||||
return await axios.post(`${ontimeURL}/settings`, data);
|
||||
};
|
||||
export const postSettings = async (data) => axios.post(`${ontimeURL}/settings`, data);
|
||||
|
||||
export const getInfo = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/info`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postInfo = async (data) => {
|
||||
return await axios.post(`${ontimeURL}/info`, data);
|
||||
};
|
||||
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
|
||||
|
||||
export const getAliases = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/aliases`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postAliases = async (data) => {
|
||||
return await axios.post(`${ontimeURL}/aliases`, data);
|
||||
export const postAliases = async (data) => axios.post(`${ontimeURL}/aliases`, data);
|
||||
|
||||
export const getUserFields = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/userfields`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postUserFields = async (data) => axios.post(`${ontimeURL}/userfields`, data);
|
||||
|
||||
export const getOSC = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/osc`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postOSC = async (data) => {
|
||||
return await axios.post(`${ontimeURL}/osc`, data);
|
||||
};
|
||||
export const postOSC = async (data) => axios.post(`${ontimeURL}/osc`, data);
|
||||
|
||||
export const downloadEvents = async () => {
|
||||
await axios({
|
||||
@@ -128,13 +140,13 @@ export const downloadEvents = async () => {
|
||||
method: 'GET',
|
||||
responseType: 'blob', // important
|
||||
}).then((response) => {
|
||||
let headerLine = response.headers['Content-Disposition'];
|
||||
const headerLine = response.headers['Content-Disposition'];
|
||||
let filename = 'events.json';
|
||||
|
||||
// try and get the filename from the response
|
||||
if (headerLine != null) {
|
||||
let startFileNameIndex = headerLine.indexOf('"') + 1;
|
||||
let endFileNameIndex = headerLine.lastIndexOf('"');
|
||||
const startFileNameIndex = headerLine.indexOf('"') + 1;
|
||||
const endFileNameIndex = headerLine.lastIndexOf('"');
|
||||
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
||||
}
|
||||
|
||||
@@ -150,14 +162,11 @@ export const downloadEvents = async () => {
|
||||
export const uploadEvents = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file); // appending file
|
||||
await axios
|
||||
.post(`${ontimeURL}/db`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
await axios.post(`${ontimeURL}/db`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadEventsWithPath = async (filepath) => {
|
||||
await axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
};
|
||||
export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
|
||||
@@ -1,37 +1,16 @@
|
||||
import axios from 'axios';
|
||||
import { playbackURL } from '../api/apiConstants';
|
||||
import { playbackURL } from './apiConstants';
|
||||
|
||||
export const getStart = async () => {
|
||||
const res = await axios.get(playbackURL + '/start');
|
||||
return res;
|
||||
};
|
||||
export const getStart = async () => axios.get(`${playbackURL}/start`);
|
||||
|
||||
export const getPause = async () => {
|
||||
const res = axios.get(playbackURL + '/pause');
|
||||
return res;
|
||||
};
|
||||
export const getPause = async () => axios.get(`${playbackURL}/pause`);
|
||||
|
||||
export const getRoll = async () => {
|
||||
const res = axios.get(playbackURL + '/roll');
|
||||
return res;
|
||||
};
|
||||
export const getRoll = async () => axios.get(`${playbackURL}/roll`);
|
||||
|
||||
export const getPrevious = async () => {
|
||||
const res = axios.get(playbackURL + '/previous');
|
||||
return res;
|
||||
};
|
||||
export const getPrevious = async () => axios.get(`${playbackURL}/previous`);
|
||||
|
||||
export const getNext = async () => {
|
||||
const res = axios.get(playbackURL + '/next');
|
||||
return res;
|
||||
};
|
||||
export const getNext = async () => axios.get(`${playbackURL}/next`);
|
||||
|
||||
export const getUnload = async () => {
|
||||
const res = axios.get(playbackURL + '/unload');
|
||||
return res;
|
||||
};
|
||||
export const getUnload = async () => axios.get(`${playbackURL}/unload`);
|
||||
|
||||
export const getReload = async () => {
|
||||
const res = axios.get(playbackURL + '/reload');
|
||||
return res;
|
||||
};
|
||||
export const getReload = async () => axios.get(`${playbackURL}/reload`);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// Exported viewer links
|
||||
const speakerLink = 'http://localhost:4001/speaker';
|
||||
const smLink = 'http://localhost:4001/sm';
|
||||
const publicLink = 'http://localhost:4001/public';
|
||||
const pipLink = 'http://localhost:4001/pip';
|
||||
const studioLink = 'http://localhost:4001/studio';
|
||||
// Exported viewer link location
|
||||
const speakerLocation = 'speaker';
|
||||
const smLocation = 'sm';
|
||||
const publicLocation = 'public';
|
||||
const pipLocation = 'pip';
|
||||
const studioLocation = 'studio';
|
||||
const cuesheetLocation = 'cuesheet';
|
||||
|
||||
export const viewerLinks = [
|
||||
{ link: speakerLink, label: 'Speaker Screen' },
|
||||
{ link: smLink, label: 'Backstage Screen' },
|
||||
{ link: publicLink, label: 'Public Screen' },
|
||||
{ link: pipLink, label: 'Picture in Picture' },
|
||||
{ link: studioLink, label: 'Studio Clock' }
|
||||
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' }
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useFetch } from '../hooks/useFetch';
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { getSettings } from '../api/ontimeApi';
|
||||
@@ -6,11 +6,11 @@ import { getSettings } from '../api/ontimeApi';
|
||||
export const AppContext = createContext({
|
||||
auth: false,
|
||||
data: {
|
||||
pinCode: null
|
||||
}
|
||||
pinCode: null,
|
||||
},
|
||||
});
|
||||
|
||||
export const AppContextProvider = (props) => {
|
||||
export const AppContextProvider = ({ children }) => {
|
||||
const [auth, setAuth] = useState(true);
|
||||
const { data } = useFetch(APP_SETTINGS, getSettings);
|
||||
|
||||
@@ -21,17 +21,16 @@ export const AppContextProvider = (props) => {
|
||||
} else {
|
||||
setAuth(false);
|
||||
}
|
||||
},[data])
|
||||
|
||||
const validate = useCallback((pin) => {
|
||||
const correct = pin === data.pinCode;
|
||||
setAuth(correct);
|
||||
return correct;
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{ auth, validate }}>
|
||||
{props.children}
|
||||
</AppContext.Provider>
|
||||
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 (typeof 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>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useSocket } from './socketContext';
|
||||
import { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { generateId } from 'ontime-utils/generate_id';
|
||||
import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
|
||||
import { generateId } from '../../common/utils/generate_id';
|
||||
import { nowInMillis, stringFromMillis } from '../../common/utils/time';
|
||||
|
||||
export const LoggingContext = createContext({
|
||||
logData: [],
|
||||
emitInfo: () => undefined,
|
||||
emitWarning: () => undefined,
|
||||
emitError: () => undefined,
|
||||
clearLog: () => undefined
|
||||
clearLog: () => undefined,
|
||||
});
|
||||
|
||||
export const LoggingProvider = (props) => {
|
||||
export const LoggingProvider = ({ children }) => {
|
||||
const MAX_MESSAGES = 100;
|
||||
const socket = useSocket();
|
||||
const [logData, setLogData] = useState([]);
|
||||
@@ -40,57 +40,69 @@ export const LoggingProvider = (props) => {
|
||||
* @param level
|
||||
* @private
|
||||
*/
|
||||
const _send = useCallback((text, level) => {
|
||||
if (socket != null) {
|
||||
const m = {
|
||||
id: generateId(),
|
||||
origin,
|
||||
time: stringFromMillis(nowInMillis()),
|
||||
level,
|
||||
text
|
||||
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);
|
||||
}
|
||||
setLogData((l) => [m, ...l]);
|
||||
socket.emit('logger', m);
|
||||
}
|
||||
if (logData.length > MAX_MESSAGES) {
|
||||
setLogData((l) => l.pop());
|
||||
}
|
||||
},[logData, socket]);
|
||||
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]);
|
||||
const emitInfo = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'INFO');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level WARN
|
||||
* @param text
|
||||
*/
|
||||
const emitWarning = useCallback((text) => {
|
||||
_send(text, 'WARN');
|
||||
}, [_send]);
|
||||
const emitWarning = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'WARN');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level ERROR
|
||||
* @param text
|
||||
*/
|
||||
const emitError = useCallback((text) => {
|
||||
_send(text, 'ERROR');
|
||||
}, [_send]);
|
||||
const emitError = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'ERROR');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Clears running log
|
||||
*/
|
||||
const clearLog = useCallback(() => {
|
||||
setLogData([])
|
||||
setLogData([]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoggingContext.Provider value = {{ emitInfo, logData, emitWarning, emitError, clearLog }}>
|
||||
{props.children}
|
||||
<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;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @returns {{catch: string, value: boolean}}
|
||||
*/
|
||||
export const validateTimes = (timeStart, timeEnd) => {
|
||||
let validate = { value: true, catch: '' };
|
||||
const validate = { value: true, catch: '' };
|
||||
if (timeStart > timeEnd) {
|
||||
validate.catch = 'Start time later than end time';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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,17 +1,17 @@
|
||||
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'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+7
-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, ...rest } = 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'}
|
||||
@@ -16,6 +17,7 @@ export default function LockIconBtn(props) {
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -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,10 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { useState } from 'react';
|
||||
import { IoRemove } from 'react-icons/io5';
|
||||
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 = () => {
|
||||
@@ -15,7 +15,7 @@ export default function DeleteIconBtn(props) {
|
||||
return (
|
||||
<Tooltip label='Delete'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { IoCloseSharp, IoCheckmarkSharp } from 'react-icons/io5';
|
||||
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 } = props;
|
||||
const { active, text, actionHandler, size } = props;
|
||||
return (
|
||||
<Button
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoPlaySkipForward } from 'react-icons/io5';
|
||||
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 (
|
||||
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipForward size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoMicSharp, IoMicOffOutline } from 'react-icons/io5';
|
||||
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, ...rest } = props;
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={active ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||
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,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoPause } from 'react-icons/io5';
|
||||
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 (
|
||||
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPause size='24px' />}
|
||||
colorScheme='orange'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoPlaySkipBack } from 'react-icons/io5';
|
||||
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 (
|
||||
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipBack size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
|
||||
@@ -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,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoReload } from 'react-icons/io5';
|
||||
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 (
|
||||
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoReload size='22px' />}
|
||||
icon={<IoArrowUndo size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoTimeOutline } from 'react-icons/io5';
|
||||
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 (
|
||||
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoTimeOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoPlay } from 'react-icons/io5';
|
||||
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 (
|
||||
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlay size='24px' />}
|
||||
colorScheme='green'
|
||||
|
||||
@@ -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,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoStop } from 'react-icons/io5';
|
||||
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 (
|
||||
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoStop size='22px' />}
|
||||
colorScheme='red'
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoSunny } from 'react-icons/io5';
|
||||
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 (
|
||||
<Tooltip label={active ? 'Make invisible' : 'Make visible'} openDelay={500}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<IoSunny size={'18px'}/>}
|
||||
size={size || 'xs'}
|
||||
icon={<IoSunny size='18px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import PropTypes from "prop-types";
|
||||
import style from "../../../features/info/Info.module.scss";
|
||||
import {Icon} from "@chakra-ui/react";
|
||||
import {FiChevronUp} from "react-icons/fi";
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import style from './CollapseBar.module.scss';
|
||||
|
||||
export default function CollapseBar(props) {
|
||||
const {title = 'Collapse bar', isCollapsed = false, onClick}= props;
|
||||
const { title = 'Collapse bar', isCollapsed, onClick, roll } = props;
|
||||
|
||||
return(
|
||||
<div className={style.header}>
|
||||
return (
|
||||
<div className={roll ? style.headerRoll : style.header}>
|
||||
{title}
|
||||
<Icon
|
||||
className={isCollapsed ? style.moreCollapsed : style.moreExpanded}
|
||||
@@ -15,10 +16,11 @@ export default function CollapseBar(props) {
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
CollapseBar.propTypes = {
|
||||
title: PropTypes.string,
|
||||
isCollapsed: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
roll: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.header,
|
||||
.header__roll {
|
||||
.headerRoll {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: #ccc;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: #ccc;
|
||||
color: $header-gray;
|
||||
}
|
||||
|
||||
.header__roll {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
.headerRoll {
|
||||
color: $ontime-roll;
|
||||
}
|
||||
|
||||
.moreExpanded,
|
||||
.moreCollapsed {
|
||||
cursor: pointer;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.moreExpanded {
|
||||
transform: scaleY(-1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.moreCollapsed {
|
||||
transform: scaleY(1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { memo } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Countdown.module.css';
|
||||
import styles from './Countdown.module.scss';
|
||||
|
||||
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
// prepare display string
|
||||
const display =
|
||||
time != null && !isNaN(time)
|
||||
? formatDisplay(time, hideZeroHours)
|
||||
: '-- : -- : --';
|
||||
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
|
||||
|
||||
const colour = isNegative ? '#ff7597' : '#fffffa';
|
||||
const classes = `${small ? styles.countdownClockSmall : styles.countdownClock}
|
||||
${isNegative ? styles.negative : ''}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={small ? styles.countdownClockSmall : styles.countdownClock}
|
||||
style={{ color: colour }}
|
||||
>
|
||||
<div className={classes}>
|
||||
{display}
|
||||
</div>
|
||||
);
|
||||
@@ -25,8 +21,8 @@ const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
export default memo(Countdown);
|
||||
|
||||
Countdown.propTypes = {
|
||||
time: PropTypes.number.isRequired,
|
||||
time: PropTypes.number,
|
||||
small: PropTypes.bool,
|
||||
isNegative: PropTypes.bool,
|
||||
hideZeroHour: PropTypes.bool,
|
||||
hideZeroHours: PropTypes.bool,
|
||||
};
|
||||
|
||||
+6
-2
@@ -1,10 +1,11 @@
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap'); */
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
/* Common */
|
||||
.countdownClock,
|
||||
.countdownClockSmall {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
line-height: 0.9;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
/* Viewers */
|
||||
@@ -21,5 +22,8 @@
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 600;
|
||||
color: #fffffa;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import style from './ErrorBoundary.module.scss';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
static contextType = LoggingContext;
|
||||
reportContent = '';
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -20,11 +23,25 @@ class ErrorBoundary extends React.Component {
|
||||
errorInfo: info,
|
||||
});
|
||||
this.context.emitError(error.toString());
|
||||
this.reportContent = `${error} ${info.componentStack}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.errorMessage) {
|
||||
return <p>:/</p>;
|
||||
return (
|
||||
<div className={style.errorContainer}>
|
||||
<div>
|
||||
<p className={style.error}>:/</p>
|
||||
<p>Something went wrong</p>
|
||||
<p
|
||||
className={style.report}
|
||||
onClick={() => navigator.clipboard.writeText(this.reportContent)}
|
||||
>
|
||||
Copy error
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.errorContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: #121212;
|
||||
color: white;
|
||||
|
||||
.error {
|
||||
color: $ontime-pink;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.report {
|
||||
text-decoration: underline $ontime-pink;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.report:hover {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
.report:active {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext } from 'react';
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -8,6 +8,13 @@ export default function EventTimes(props) {
|
||||
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -18,8 +25,10 @@ export default function EventTimes(props) {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else if (entry === 'durationOverride'){
|
||||
return true;
|
||||
} else {
|
||||
return;
|
||||
return false
|
||||
}
|
||||
|
||||
const valid = validateTimes(start, end);
|
||||
@@ -54,8 +63,8 @@ export default function EventTimes(props) {
|
||||
|
||||
EventTimes.propTypes = {
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,117 +1,21 @@
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { useContext } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const label = {
|
||||
fontSize: '0.75em',
|
||||
color: '#aaa',
|
||||
};
|
||||
|
||||
const TimesDelayed = (props) => {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span style={label}>
|
||||
Start <span>{scheduledStart}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const Times = (props) => {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span style={label}>Start</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>End</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
};
|
||||
import TimesDelayed from './TimesDelayed';
|
||||
import Times from './Times';
|
||||
|
||||
export default function EventTimesVertical(props) {
|
||||
const { delay, timeStart, timeEnd, duration, previousEnd } = 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;
|
||||
@@ -122,9 +26,7 @@ export default function EventTimesVertical(props) {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else return entry === 'durationOverride';
|
||||
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
@@ -134,10 +36,10 @@ export default function EventTimesVertical(props) {
|
||||
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}
|
||||
@@ -147,7 +49,7 @@ export default function EventTimesVertical(props) {
|
||||
) : (
|
||||
<Times
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
actionHandler={actionHandler}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
@@ -157,9 +59,10 @@ export default function EventTimesVertical(props) {
|
||||
}
|
||||
|
||||
EventTimesVertical.propTypes = {
|
||||
delay: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
};
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import EditableTimer from '../../input/EditableTimer';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Times.module.scss'
|
||||
|
||||
export default function Times(props) {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>Start</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>End</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 0.75em;
|
||||
color: $label-gray;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import EditableTimer from '../../input/EditableTimer';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
import style from './Times.module.scss'
|
||||
|
||||
export default function TimesDelayed(props) {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>
|
||||
Start <span>{scheduledStart}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
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,
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { clamp } from 'app/utils/math';
|
||||
import styles from './MyProgressBar.module.css';
|
||||
import styles from './MyProgressBar.module.scss';
|
||||
|
||||
export default function MyProgressBar(props) {
|
||||
const { now, complete, showElapsed } = props;
|
||||
|
||||
+5
-4
@@ -1,22 +1,23 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.progress,
|
||||
.progressCountdown {
|
||||
height: 2vh;
|
||||
background-color: rgba(255, 255, 255, 0.13);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: rgba(255, 255, 255, 0.13);
|
||||
background-color: $bg-gray-900;
|
||||
}
|
||||
|
||||
.progressCountdown {
|
||||
background-color: #ff7597;
|
||||
background-color: $ontime-pink;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
height: 2vh;
|
||||
background-color: rgb(255, 255, 255);
|
||||
background-color: $title-white;
|
||||
border-radius: 4px;
|
||||
transition: 1s linear;
|
||||
transition-property: width;
|
||||
@@ -1,13 +1,13 @@
|
||||
import PropTypes from "prop-types";
|
||||
import React, { useCallback, useEffect, useState } 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, useCallback } from 'react';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import style from './NavLogo.module.scss';
|
||||
|
||||
export default function NavLogo(props) {
|
||||
const {isHidden} = props;
|
||||
const { isHidden } = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -34,74 +34,43 @@ export default function NavLogo(props) {
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
const baseOpacity = (isHidden) ? 0 : 0.5
|
||||
const baseOpacity = isHidden ? 0 : 0.5;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: baseOpacity }}
|
||||
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
className={style.navContainer}
|
||||
>
|
||||
<Image
|
||||
alt=''
|
||||
src={navlogo}
|
||||
className={style.logo}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
|
||||
<AnimatePresence>
|
||||
{showNav && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0, y: -50 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, scaleY: 0, y: -50 }}
|
||||
className={showNav ? style.nav : style.navHidden}
|
||||
className={style.nav}
|
||||
>
|
||||
<Link
|
||||
to='/timer'
|
||||
className={style.navItem}
|
||||
tabIndex={1}
|
||||
>
|
||||
<Link to='/timer' className={style.navItem} tabIndex={1}>
|
||||
Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/minimal'
|
||||
className={style.navItem}
|
||||
tabIndex={2}
|
||||
>
|
||||
<Link to='/minimal' className={style.navItem} tabIndex={2}>
|
||||
Minimal Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/sm'
|
||||
className={style.navItem}
|
||||
tabIndex={3}
|
||||
>
|
||||
<Link to='/sm' className={style.navItem} tabIndex={3}>
|
||||
Backstage
|
||||
</Link>
|
||||
<Link
|
||||
to='/public'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
>
|
||||
<Link to='/public' className={style.navItem} tabIndex={4}>
|
||||
Public
|
||||
</Link>
|
||||
<Link
|
||||
to='/lower'
|
||||
className={style.navItem}
|
||||
tabIndex={5}
|
||||
>
|
||||
<Link to='/lower' className={style.navItem} tabIndex={5}>
|
||||
Lower Thirds
|
||||
</Link>
|
||||
<Link
|
||||
to='/pip'
|
||||
className={style.navItem}
|
||||
tabIndex={6}
|
||||
>
|
||||
<Link to='/pip' className={style.navItem} tabIndex={6}>
|
||||
PIP
|
||||
</Link>
|
||||
<Link
|
||||
to='/studio'
|
||||
className={style.navItem}
|
||||
tabIndex={7}
|
||||
>
|
||||
<Link to='/studio' className={style.navItem} tabIndex={7}>
|
||||
Studio Clock
|
||||
</Link>
|
||||
</motion.div>
|
||||
@@ -113,4 +82,4 @@ export default function NavLogo(props) {
|
||||
|
||||
NavLogo.propTypes = {
|
||||
isHidden: PropTypes.bool,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$nav-color: #fff;
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.navContainer {
|
||||
position: absolute;
|
||||
@@ -9,22 +9,24 @@ $nav-color: #fff;
|
||||
align-items: flex-end;
|
||||
font-size: max(1vw, 16px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav {
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav,
|
||||
.showNav {
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.navItem {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $nav-color;
|
||||
.navItem {
|
||||
background-color: $bg-overlay;
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,91 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './ProtectRoute.module.scss';
|
||||
import { PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiCheck } from 'react-icons/fi';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import { AppContext } from '../../../app/context/AppContext';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
||||
|
||||
export default function ProtectRoute(props) {
|
||||
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||
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);
|
||||
|
||||
const handleValidation = useCallback(() => {
|
||||
const r = validate(pin);
|
||||
if (!r) {
|
||||
setFailed(true);
|
||||
setPin('');
|
||||
}
|
||||
}, [pin, validate]);
|
||||
|
||||
// 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>
|
||||
) : (
|
||||
props.children
|
||||
)}
|
||||
</>
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Space bar
|
||||
if (e.keyCode === 13) {
|
||||
handleValidation();
|
||||
}
|
||||
},
|
||||
[handleValidation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
if (isLocal || auth) {
|
||||
return children;
|
||||
} else {
|
||||
return (
|
||||
<div className={style.container}>
|
||||
ontime
|
||||
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
|
||||
<PinInput
|
||||
type='alphanumeric'
|
||||
size='lg'
|
||||
mask
|
||||
autoFocus
|
||||
value={pin}
|
||||
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()}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProtectRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@use '../../../styles/mixins' as *;
|
||||
|
||||
.container {
|
||||
background: #222;
|
||||
background: $bg-black;
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
@@ -18,8 +18,6 @@
|
||||
|
||||
.pin,
|
||||
.pin__failed {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
|
||||
input {
|
||||
|
||||
@@ -1,32 +1,53 @@
|
||||
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';
|
||||
import style from './Paginator.module.scss';
|
||||
import Empty from '../../state/Empty';
|
||||
|
||||
export default function Paginator(props) {
|
||||
const { events, selectedId } = props;
|
||||
const LIMIT_PER_PAGE = props.limit || 8;
|
||||
const SCROLL_TIME = props.time * 1000 || 10000;
|
||||
const {
|
||||
events,
|
||||
selectedId,
|
||||
limit = 8,
|
||||
time = 10,
|
||||
isBackstage,
|
||||
setPageNumber,
|
||||
setCurrentPage,
|
||||
} = props;
|
||||
const LIMIT_PER_PAGE = limit;
|
||||
const SCROLL_TIME = time * 1000;
|
||||
const [numEvents, setNumEvents] = useState(0);
|
||||
const [page, setPage] = useState([]);
|
||||
const [pages, setPages] = useState(0);
|
||||
const [selPage, setSelPage] = useState(0);
|
||||
|
||||
// keep parent up to date
|
||||
useEffect(() => {
|
||||
if (setPageNumber) {
|
||||
setPageNumber(pages);
|
||||
}
|
||||
}, [setPageNumber, pages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setCurrentPage) {
|
||||
setCurrentPage(selPage);
|
||||
}
|
||||
}, [setCurrentPage, selPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (events == null) return;
|
||||
// how many events in list
|
||||
let n = events.length;
|
||||
const n = events.length;
|
||||
setNumEvents(n);
|
||||
|
||||
// how many paginated views
|
||||
let p = Math.ceil(n / LIMIT_PER_PAGE);
|
||||
setPages(p);
|
||||
setPages(Math.ceil(n / LIMIT_PER_PAGE));
|
||||
|
||||
// divide events in parts of LIMIT_PER_PAGE
|
||||
const eventStart = LIMIT_PER_PAGE * selPage;
|
||||
const eventEnd = LIMIT_PER_PAGE * (selPage + 1);
|
||||
let e = events.slice(eventStart, eventEnd);
|
||||
setPage(e);
|
||||
setPage(events.slice(eventStart, eventEnd));
|
||||
|
||||
// if array is completely in past, show depending on SCROLL_PAST
|
||||
}, [events, selPage, LIMIT_PER_PAGE]);
|
||||
@@ -41,17 +62,10 @@ export default function Paginator(props) {
|
||||
|
||||
let selectedState = 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.nav}>
|
||||
{pages > 1 &&
|
||||
[...Array(pages)].map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === selPage ? style.navItemSelected : style.navItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
if (events?.length < 1) {
|
||||
return <Empty text='No events to show' />;
|
||||
} else {
|
||||
return (
|
||||
<div className={style.entries}>
|
||||
{page.map((e) => {
|
||||
if (e.id === selectedId) selectedState = 1;
|
||||
@@ -63,11 +77,22 @@ export default function Paginator(props) {
|
||||
timeStart={e.timeStart}
|
||||
timeEnd={e.timeEnd}
|
||||
title={e.title}
|
||||
colour={isBackstage ? e.colour : ''}
|
||||
backstageEvent={!e.isPublic}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Paginator.propTypes = {
|
||||
events: PropTypes.array,
|
||||
selectedId: PropTypes.string,
|
||||
limit: PropTypes.number,
|
||||
time: PropTypes.number,
|
||||
isBackstage: PropTypes.bool,
|
||||
setPageNumber: PropTypes.func,
|
||||
setCurrentPage: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
.entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navItem,
|
||||
.navItemSelected {
|
||||
background-color: rgba(255, 255, 255, 0.39);
|
||||
width: 0.7vw;
|
||||
height: 0.7vw;
|
||||
border-radius: 0.35vw;
|
||||
}
|
||||
|
||||
.navItemSelected {
|
||||
background-color: rgba(255, 255, 255, 0.79);
|
||||
}
|
||||
|
||||
.nav {
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-bottom: 2.5vh;
|
||||
}
|
||||
|
||||
.nav > div {
|
||||
margin-left: 0.5vw;
|
||||
}
|
||||
|
||||
.entryTimes {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entryPast {
|
||||
background-color: rgba(255, 255, 255, 0.01);
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
.entryPast > .entryTitle {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.entryFuture {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.entryNow {
|
||||
color: #fff;
|
||||
line-height: 5vh;
|
||||
background-color: rgba(255, 255, 255, 0.07);
|
||||
border-bottom: 0.5vh solid #ff7597aa;
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.backstageInd {
|
||||
color: #ff7597aa;
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
}
|
||||
|
||||
.backstageInd::before {
|
||||
content: '*';
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.entries {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entryTimes {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entryPast {
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
.entryPast > .entryTitle {
|
||||
color: $text-gray-disabled;
|
||||
}
|
||||
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.entryFuture {
|
||||
background-color: $bg-gray-950;
|
||||
}
|
||||
|
||||
.entryNow {
|
||||
line-height: 5vh;
|
||||
background-color: $bg-gray-900;
|
||||
border-bottom: 0.5vh solid $ontime-pink;
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.backstageInd {
|
||||
color: $ontime-pink;
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
}
|
||||
|
||||
.backstageInd::before {
|
||||
content: '*';
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import style from './TitleCard.module.css';
|
||||
import React from 'react';
|
||||
import style from './TitleCard.module.scss';
|
||||
|
||||
export default function TitleCard(props) {
|
||||
const { label, title, subtitle, presenter } = props;
|
||||
|
||||
+6
-9
@@ -1,27 +1,24 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
@include card-label;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle,
|
||||
.presenter {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ddd;
|
||||
@include card-title;
|
||||
font-size: 2.5vw;
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
|
||||
}
|
||||
|
||||
.subtitle,
|
||||
.presenter {
|
||||
color: #aaa;
|
||||
color: $subtitle-gray;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -1,4 +1,5 @@
|
||||
import style from './TitleSide.module.css';
|
||||
import React from 'react';
|
||||
import style from './TitleSide.module.scss';
|
||||
|
||||
export default function TitleSide(props) {
|
||||
const { type, label, title, subtitle, presenter } = props;
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
@include card-label;
|
||||
}
|
||||
|
||||
.nowTitle,
|
||||
.nextTitle {
|
||||
color: #ddd;
|
||||
font-weight: 600;
|
||||
@include card-title;
|
||||
}
|
||||
|
||||
.nowSubtitle,
|
||||
.nowPresenter,
|
||||
.nextSubtitle,
|
||||
.nextPresenter {
|
||||
color: #aaa;
|
||||
color: $subtitle-gray;
|
||||
}
|
||||
|
||||
.nowTitle {
|
||||
@@ -1,25 +1,38 @@
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import style from './Paginator.module.css';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
import style from './Paginator.module.scss';
|
||||
|
||||
export default function TodayItem(props) {
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent } = 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 : ''}`}>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className={style.entryTitle}>{title}</div>
|
||||
{backstageEvent && <div className={style.backstageInd}/>}
|
||||
{backstageEvent && <div className={style.backstageInd} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TodayItem.propTypes = {
|
||||
selected: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
backstageEvent: PropTypes.bool,
|
||||
colour: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -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,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,21 +1,27 @@
|
||||
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 || '');
|
||||
const maxchar = props.maxchar || 40;
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue == null) setText('');
|
||||
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) => {
|
||||
@@ -33,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: $bg-gray-500;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,30 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
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';
|
||||
import { stringFromMillis } from '../utils/time';
|
||||
import style from './EditableTimer.module.scss';
|
||||
|
||||
export default function EditableTimer(props) {
|
||||
const { name, actionHandler, time, delay, validate, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
// prepare time fields
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [time, delay, emitError]);
|
||||
|
||||
const validateValue = (value) => {
|
||||
const success = handleSubmit(value);
|
||||
if (success) setValue(value);
|
||||
else setValue(stringFromMillis(time + delay, true));
|
||||
};
|
||||
|
||||
const handleSubmit = (value) => {
|
||||
// Check if there is anything there
|
||||
if (value === '') return false;
|
||||
|
||||
let newValMillis;
|
||||
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 {
|
||||
newValMillis = 0;
|
||||
}
|
||||
} else if (value.startsWith('+')) {
|
||||
} 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);
|
||||
@@ -65,16 +48,39 @@ export default function EditableTimer(props) {
|
||||
return true;
|
||||
};
|
||||
|
||||
// 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));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [time, delay, emitError]);
|
||||
|
||||
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}
|
||||
className={delay > 0 ? style.delayedEditable : style.editable}
|
||||
className={isDelayed ? style.delayedEditable : style.editable}
|
||||
>
|
||||
<EditablePreview />
|
||||
<EditableInput type='text' placeholder='--:--:--' />
|
||||
<EditableInput type='text' placeholder='--:--:--' data-testid='editable-timer-input' />
|
||||
</Editable>
|
||||
);
|
||||
}
|
||||
@@ -82,8 +88,8 @@ export default function EditableTimer(props) {
|
||||
EditableTimer.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
time: PropTypes.number.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
time: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
validate: PropTypes.func.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
.editable,
|
||||
.delayedEditable {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
width: 6.5em;
|
||||
letter-spacing: 1px;
|
||||
height: fit-content;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.delayedEditable {
|
||||
border: 1px solid #d69e2e55;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@use '../../styles/main'as *;
|
||||
|
||||
.editable,
|
||||
.delayedEditable {
|
||||
background-color: $input-bg;
|
||||
border: $input-border;
|
||||
width: 6.5em;
|
||||
letter-spacing: 1px;
|
||||
height: fit-content;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.delayedEditable {
|
||||
border: $input-delayed-border;
|
||||
}
|
||||
@@ -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,12 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
||||
import style from './Empty.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Empty.module.scss';
|
||||
|
||||
export default function Empty(props) {
|
||||
const { text } = props;
|
||||
const { text, ...rest } = props;
|
||||
return (
|
||||
<div className={style.emptyContainer}>
|
||||
<div className={style.emptyContainer} {...rest}>
|
||||
<Emptyimage className={style.empty} />
|
||||
<span className={style.text}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Empty.propTypes = {
|
||||
text: PropTypes.string,
|
||||
}
|
||||
+3
-3
@@ -1,3 +1,5 @@
|
||||
@use '../../styles/main' as *;
|
||||
|
||||
.emptyContainer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
@@ -5,13 +7,11 @@
|
||||
|
||||
.empty {
|
||||
width: 100%;
|
||||
margin: auto 0;
|
||||
margin-top: 10vh;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: rgba(0, 0, 0, 0.17);
|
||||
color: $bg-black-300;
|
||||
font-weight: 600;
|
||||
font-size: 2em;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
forgivingStringToMillis,
|
||||
formatDisplay,
|
||||
isTimeString,
|
||||
millisToMinutes,
|
||||
millisToSeconds,
|
||||
forgivingStringToMillis,
|
||||
timeStringToMillis,
|
||||
} from '../dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { stringFromMillis } from '../time';
|
||||
|
||||
describe('test string from formatDisplay function', () => {
|
||||
it('test with null values', () => {
|
||||
@@ -280,33 +280,87 @@ describe('test isTimeString() function handle different separators', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('test timeHelper() function handles separators', () => {
|
||||
const ts = ['1:2:3:10', '2,10', '2.10'];
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s)).toBe('number');
|
||||
});
|
||||
}
|
||||
});
|
||||
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 },
|
||||
];
|
||||
|
||||
describe('test timeHelper() 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 testData) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,111 +1,122 @@
|
||||
import {formatEventList, getEventsWithDelay, trimEventlist} from "../eventsManager";
|
||||
import { formatEventList, getEventsWithDelay, trimEventlist } from '../eventsManager';
|
||||
|
||||
test('getEventsWithDelay function', () => {
|
||||
|
||||
const testData = [
|
||||
{
|
||||
"title": "Welcome to Ontime",
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
"duration": 60000,
|
||||
"type": "delay",
|
||||
"id": "24240"
|
||||
duration: 60000,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000,
|
||||
"timeEnd": 35520000,
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
{
|
||||
"title": "Use simpler times to create a timer",
|
||||
"timeStart": 120000,
|
||||
"timeEnd": 720000,
|
||||
"type": "event",
|
||||
"id": "8222"
|
||||
title: 'Use simpler times to create a timer',
|
||||
timeStart: 120000,
|
||||
timeEnd: 720000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8222',
|
||||
},
|
||||
{
|
||||
"duration": 900000,
|
||||
"type": "delay",
|
||||
"revision": 0,
|
||||
"id": "a386"
|
||||
duration: 900000,
|
||||
type: 'delay',
|
||||
revision: 0,
|
||||
id: 'a386',
|
||||
},
|
||||
{
|
||||
"title": "Add delay blocks to affect all events",
|
||||
"timeStart": 37320000,
|
||||
"timeEnd": 38520000,
|
||||
"type": "event",
|
||||
"id": "6dce"
|
||||
title: 'Add delay blocks to affect all events',
|
||||
timeStart: 37320000,
|
||||
timeEnd: 38520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '6dce',
|
||||
},
|
||||
{
|
||||
"title": "Add and remove events with [+] and [-]",
|
||||
"timeStart": 38520000,
|
||||
"timeEnd": 45120000,
|
||||
"type": "event",
|
||||
"id": "2651"
|
||||
title: 'Add and remove events with [+] and [-]',
|
||||
timeStart: 38520000,
|
||||
timeEnd: 45120000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '2651',
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "e6a1"
|
||||
type: 'block',
|
||||
id: 'e6a1',
|
||||
},
|
||||
{
|
||||
"title": "And control whether they are public",
|
||||
"timeStart": 46800000,
|
||||
"timeEnd": 57600000,
|
||||
"type": "event",
|
||||
"id": "1358"
|
||||
}
|
||||
title: 'And control whether they are public',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '1358',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
"title": "Welcome to Ontime",
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000+60000,
|
||||
"timeEnd": 35520000+60000,
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000 + 60000,
|
||||
timeEnd: 35520000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
{
|
||||
"title": "Use simpler times to create a timer",
|
||||
"timeStart": 120000+60000,
|
||||
"timeEnd": 720000+60000,
|
||||
"type": "event",
|
||||
"id": "8222"
|
||||
title: 'Use simpler times to create a timer',
|
||||
timeStart: 120000 + 60000,
|
||||
timeEnd: 720000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8222',
|
||||
},
|
||||
{
|
||||
"title": "Add delay blocks to affect all events",
|
||||
"timeStart": 37320000+60000+900000,
|
||||
"timeEnd": 38520000+60000+900000,
|
||||
"type": "event",
|
||||
"id": "6dce"
|
||||
title: 'Add delay blocks to affect all events',
|
||||
timeStart: 37320000 + 60000 + 900000,
|
||||
timeEnd: 38520000 + 60000 + 900000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '6dce',
|
||||
},
|
||||
{
|
||||
"title": "Add and remove events with [+] and [-]",
|
||||
"timeStart": 38520000+60000+900000,
|
||||
"timeEnd": 45120000+60000+900000,
|
||||
"type": "event",
|
||||
"id": "2651"
|
||||
title: 'Add and remove events with [+] and [-]',
|
||||
timeStart: 38520000 + 60000 + 900000,
|
||||
timeEnd: 45120000 + 60000 + 900000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '2651',
|
||||
},
|
||||
{
|
||||
"title": "And control whether they are public",
|
||||
"timeStart": 46800000,
|
||||
"timeEnd": 57600000,
|
||||
"type": "event",
|
||||
"id": "1358"
|
||||
}
|
||||
]
|
||||
title: 'And control whether they are public',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '1358',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
@@ -135,6 +146,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
{
|
||||
"title": "Welcome to Ontime",
|
||||
"timeEnd": 30600000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
},
|
||||
@@ -147,6 +159,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000,
|
||||
"timeEnd": 35520000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
}
|
||||
@@ -155,13 +168,15 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
{
|
||||
"title": "Welcome to Ontime",
|
||||
"timeEnd": 30600000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000+60000,
|
||||
"timeEnd": 35520000+60000,
|
||||
"timeStart": 34920000 + 60000,
|
||||
"timeEnd": 35520000 + 60000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
}
|
||||
@@ -176,6 +191,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Welcome to Ontime",
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
},
|
||||
@@ -187,6 +203,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000,
|
||||
"timeEnd": 35520000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
}
|
||||
@@ -196,6 +213,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Welcome to Ontime",
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
},
|
||||
@@ -203,6 +221,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000,
|
||||
"timeEnd": 35520000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
}
|
||||
@@ -216,32 +235,32 @@ describe('test trimEventlist function', () => {
|
||||
|
||||
const limit = 8;
|
||||
const testData = [
|
||||
{id: '1'},
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'},
|
||||
{id: '9'},
|
||||
{id: '10'},
|
||||
{id: '11'},
|
||||
{id: '12'},
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
{ id: '10' },
|
||||
{ id: '11' },
|
||||
{ id: '12' },
|
||||
];
|
||||
|
||||
|
||||
test('when we use the first item', () => {
|
||||
const selectedId = '1';
|
||||
const expected = [
|
||||
{id: '1'},
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'},
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimEventlist(testData, selectedId, limit);
|
||||
@@ -252,14 +271,14 @@ describe('test trimEventlist function', () => {
|
||||
test('when we use the third item', () => {
|
||||
const selectedId = '3';
|
||||
const expected = [
|
||||
{id: '1'},
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'}
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' }
|
||||
];
|
||||
|
||||
const l = trimEventlist(testData, selectedId, limit);
|
||||
@@ -270,14 +289,14 @@ describe('test trimEventlist function', () => {
|
||||
test('when we use the fourth item', () => {
|
||||
const selectedId = '4';
|
||||
const expected = [
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'},
|
||||
{id: '9'}
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' }
|
||||
];
|
||||
|
||||
const l = trimEventlist(testData, selectedId, limit);
|
||||
@@ -288,14 +307,14 @@ describe('test trimEventlist function', () => {
|
||||
test('if selected is not found', () => {
|
||||
const selectedId = '15';
|
||||
const expected = [
|
||||
{id: '1'},
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'},
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimEventlist(testData, selectedId, limit);
|
||||
@@ -314,25 +333,27 @@ describe('test formatEvents function', () => {
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"isPublic": false,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "5946"
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"subtitle": "",
|
||||
"presenter": "",
|
||||
"note": "In green, below",
|
||||
"timeStart": 34800000,
|
||||
"timeEnd": 35400000,
|
||||
"isPublic": false,
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "8ee5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"subtitle": "",
|
||||
"presenter": "",
|
||||
"note": "In green, below",
|
||||
"timeStart": 34800000,
|
||||
"timeEnd": 35400000,
|
||||
"isPublic": false,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "8ee5"
|
||||
}
|
||||
];
|
||||
|
||||
test ('it parses correctly', () => {
|
||||
test('it parses correctly', () => {
|
||||
const selectedId = 'otherEvent';
|
||||
const nextId = 'notHere';
|
||||
const expected = [
|
||||
@@ -342,6 +363,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: ""
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
@@ -349,6 +371,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: "",
|
||||
},
|
||||
|
||||
]
|
||||
@@ -357,7 +380,7 @@ describe('test formatEvents function', () => {
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
test ('it handles selected correctly', () => {
|
||||
test('it handles selected correctly', () => {
|
||||
const selectedId = '5946';
|
||||
const nextId = '8ee5';
|
||||
const expected = [
|
||||
@@ -367,6 +390,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: "",
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
@@ -374,15 +398,16 @@ describe('test formatEvents function', () => {
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: true,
|
||||
colour: "",
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId,true);
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, true);
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
test ('it handles next correctly', () => {
|
||||
test('it handles next correctly', () => {
|
||||
const selectedId = '8ee5';
|
||||
const nextId = 'notHere';
|
||||
|
||||
@@ -393,6 +418,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: "",
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
@@ -400,6 +426,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: "",
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
@@ -21,7 +21,7 @@ export function formatDisplay(seconds, hideZero = false) {
|
||||
const minutes = Math.floor((s % 3600) / 60);
|
||||
|
||||
if (hideZero && hours < 1) return [minutes, s % 60].map(format).join(':');
|
||||
else return [hours, minutes, s % 60].map(format).join(':');
|
||||
return [hours, minutes, s % 60].map(format).join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ export const timeStringToMillis = (string) => {
|
||||
if (time.length === 1) return Math.abs(time[0] * mts);
|
||||
if (time.length === 2) return Math.abs(time[0]) * mtm + time[1] * mts;
|
||||
if (time.length === 3) return Math.abs(time[0]) * mth + time[1] * mtm + time[2] * mts;
|
||||
else return 0;
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -86,28 +86,39 @@ const parse = (valueAsString) => {
|
||||
|
||||
/**
|
||||
* @description Parses a time string to millis
|
||||
* @param string - time string
|
||||
* @param {string} value - time string
|
||||
* @returns {number} - time string in millis
|
||||
*/
|
||||
export const forgivingStringToMillis = (string) => {
|
||||
export const forgivingStringToMillis = (value) => {
|
||||
let millis = 0;
|
||||
|
||||
// split string at known separators : , .
|
||||
const separatorRegex = /[\s,:.]+/;
|
||||
const [first, second, third] = string.split(separatorRegex);
|
||||
const [first, second, third] = value.split(separatorRegex);
|
||||
|
||||
if (first != null && second != null && third != null) {
|
||||
// if string has three sections, treat as [hours] [minutes] [seconds]
|
||||
millis = parse(first) * mth;
|
||||
millis += parse(second) * mtm;
|
||||
millis += parse(third) * mts;
|
||||
} else if (third == null) {
|
||||
} else if (first != null && second != null && third == null) {
|
||||
// if string has two sections, treat as [minutes] [seconds]
|
||||
millis = parse(first) * mtm;
|
||||
millis += parse(second) * mts;
|
||||
} else if (second == null) {
|
||||
// if string has one section, treat as [minutes]
|
||||
millis = parse(first) * mtm;
|
||||
} else if (first != null && second == null && third == null) {
|
||||
// if string has one section,
|
||||
// could be a complete string like 121010 - 12:10:10
|
||||
if (first.length === 6) {
|
||||
const hours = first.substring(0, 2);
|
||||
const minutes = first.substring(2, 4);
|
||||
const seconds = first.substring(4);
|
||||
millis = parse(hours) * mth;
|
||||
millis += parse(minutes) * mtm;
|
||||
millis += parse(seconds) * mts;
|
||||
} else {
|
||||
// otherwise lets treat as [minutes]
|
||||
millis = parse(first) * mtm;
|
||||
}
|
||||
}
|
||||
return millis;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { stringFromMillis } from './time';
|
||||
|
||||
/**
|
||||
* @description From a list of events, returns only events of type event with calculated delays
|
||||
* @param {Object[]} events - given events
|
||||
* @returns {Object[]} Filtered events with calculated delays
|
||||
*/
|
||||
export const getEventsWithDelay = (events) => {
|
||||
|
||||
export const getEventsWithDelay = (events) => {
|
||||
if (events == null) return [];
|
||||
|
||||
const unfilteredEvents = [...events];
|
||||
@@ -43,8 +43,11 @@ export const trimEventlist = (events, selectedId, limit) => {
|
||||
if (limit != null) {
|
||||
while (trimmedEvents.length > limit) {
|
||||
const idx = trimmedEvents.findIndex((e) => e.id === selectedId);
|
||||
if (idx <= BEFORE) { trimmedEvents.pop(); }
|
||||
else { trimmedEvents.shift(); }
|
||||
if (idx <= BEFORE) {
|
||||
trimmedEvents.pop();
|
||||
} else {
|
||||
trimmedEvents.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
return trimmedEvents;
|
||||
@@ -64,7 +67,7 @@ export const formatEventList = (events, selectedId, nextId, showEnd = false) =>
|
||||
const givenEvents = [...events];
|
||||
|
||||
// format list
|
||||
let formattedEvents = [];
|
||||
const formattedEvents = [];
|
||||
for (const g of givenEvents) {
|
||||
const start = stringFromMillis(g.timeStart, false);
|
||||
const end = stringFromMillis(g.timeEnd, false);
|
||||
@@ -75,10 +78,9 @@ export const formatEventList = (events, selectedId, nextId, showEnd = false) =>
|
||||
title: g.title,
|
||||
isNow: g.id === selectedId,
|
||||
isNext: g.id === nextId,
|
||||
colour: g.colour,
|
||||
});
|
||||
}
|
||||
|
||||
return formattedEvents;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Handles link to external URLs: specifically for a electron / browser case
|
||||
* If electron: ask main process to call a new browser window
|
||||
* If browser: open in new tab
|
||||
* @param url
|
||||
*/
|
||||
export default function handleLink(url) {
|
||||
if (window.process?.type === 'renderer') {
|
||||
window.ipcRenderer.send('send-to-link', url);
|
||||
} else {
|
||||
window.open(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Returns hostname
|
||||
* @type {string}
|
||||
*/
|
||||
export const host = window?.location?.host;
|
||||
|
||||
/**
|
||||
* Open an external URLs: specifically for a electron / browser case
|
||||
* If electron: ask main process to call a new browser window
|
||||
* If browser: open in new tab
|
||||
* @param url
|
||||
*/
|
||||
export function openLink(url) {
|
||||
if (window.process?.type === 'renderer') {
|
||||
window.ipcRenderer.send('send-to-link', url);
|
||||
} else {
|
||||
window.open(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles opening external links
|
||||
* @param event
|
||||
* @param location
|
||||
*/
|
||||
export function handleLinks(event, location) {
|
||||
// we handle the link manually
|
||||
event.preventDefault();
|
||||
openLink(`http://${host}/${location}`);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user