Compare commits

...

39 Commits

Author SHA1 Message Date
Carlos Valente d86afee477 Feat/205 (#207)
* feat(text-background): add discrete text background to <Minimal>
* refactor: convert <Minimal> to typescript
* feat(clock): create <Clock> view
* feat(clock): version bump
2022-09-20 21:19:17 +02:00
Carlos Valente 70436059d8 Feat/132 (#198) 2022-09-19 21:15:06 +02:00
Carlos Valente fc0cbf6134 fix(203): issue with userfields re-rendering (#206) 2022-09-17 23:32:22 +02:00
Carlos Valente 3128f5a195 Fix/195 (#200)
* refactor: add typescript dependencies
* chore: update chakra-ui
* refactor: typescript config
* refactor: convert <MenuBar> component to typescript
* refactor: convert <TooltipActionBtn> component to typescript
* refactor: improve UX in file upload
* refactor: upgrade dependencies
* refactor: prepare data provider
* refactor: prevent importing bad fields
* refactor: extract merge to provider
* refactor(upload): parser merges only given fields
* refactor(upload): add event fields to excel
* refactor(upload): improve styling on modal open
* style: improve styling in menu
* style: prevent global pollution
* feat(upload): add upload options
* fix: avoid potential bug in log queue
2022-09-17 23:28:52 +02:00
Carlos Valente 94b03312a6 Refactor/styles (#202)
* style: general micro improvements in editor
* version bump
2022-09-14 21:50:14 +02:00
Carlos Valente b7e758bf24 Chore/ts (#199)
* chore: add typescript and dependencies
* refactor: remove react imports
* refactor: fix entrypoint
* chore: upgrade chakra and deps
* chore: configure eslint
2022-09-14 21:01:58 +02:00
Carlos Valente e40d750673 Doc/downloads (#201) 2022-09-14 15:46:04 +02:00
Carlos Valente b384284175 Refact/feature sockets (#193)
* chore: migrate and upgrade state dependencies
* refactor(eventlist): get data from store
* refactor(messagecontrol): get data from store
* refactor(playbackcontrol): get data from store
* refactor(info): get data from store
* refactor(messagecontrol): handle mutations in hook
* feat(playbackcontrol): get data from store
* refactor(playbackcontrol): handle mutations in hook
* refactor(playbackcontrol): simplify object
* refactor: simplify interfaces
* refactor(InputRow): mixed control + styles
* refactor(socket): cleanup unused endpoints
* refactor(table): prepare table socket endpoint
* refactor(timer): extract timer to own endpoint
* refactor(table): get data from store
* chore: update tests
* refactor(socket): update sockets on cycle
* refactor: remove unnecessary safeguards
2022-09-06 21:11:24 +02:00
Carlos Valente 0e450cb6cb Feat/table part1 (#197)
* feat(csv): export data as csv file
* feat(table): toggle fullscreen
* ux: coordinate tooltip open delay
* feat(excelDates): update tests
* refactor: folder structure
2022-09-04 22:44:03 +02:00
Carlos Valente 0651777055 Feat/161 (#186)
* feat(time): add optional time format
* feat(time): show 12 hour time in stage timer
* feat(time): override locally
* feat(time): show timer in selected format
* feat(time): show 12 hour time in table header
* refactor: extract time formatting into utility
* refactor: migrate datetime library
* refactor(formatTime): centralise time formatting for viewers
* feature(12hour): version bump
2022-08-30 19:04:02 +02:00
Carlos Valente a8ce6f3fc1 Prefeat/companion (#192)
* feat: register missing load event endpoints in websocket
* refactor: rename message socket endpoints
* version bump
2022-08-10 13:02:43 +02:00
Carlos Valente b80ae543a4 Prefeat/skip (#188)
* refactor: refactor size of event list
* refactor: optimise event mutations
* refactor: prepare skip feature
* refactor: update tests
* refactor: simplify type assertion
2022-08-09 14:02:46 +02:00
Carlos Valente 9b5d536378 Refact/project (#189)
* refactor: cleanup project entry point
* refactor: remove unused
* refactor: folder restructure
2022-08-03 23:20:23 +02:00
Carlos Valente 05be0c1e95 Feat/162 (#187)
* feat(key): add user options to minimal timer
* feat(key): register minimal timer in settings
* feat(key): version bump
2022-08-03 23:01:03 +02:00
Carlos Valente 18f3f3643b security(server): disable x-powered-by headers (#181)
* security(server): disable x-powered-by headers
2022-07-23 22:14:50 +02:00
Carlos Valente a757058511 Fix/178 (#179)
* chore(DND): upgrade dependencies
* fix(EditableCell): fix bad prop
* fix(EditableCell): fix reordering bug with hidden columns
2022-07-18 20:17:15 +02:00
Carlos Valente a20909b882 Feat/173 (#177)
* feat/173: update dependencies
* feat/173: migrate to playback requests to POST
* feat/173: HTTP startById and startByIndex
* feat/173: HTTP loadById and loadByIndex
* feat/173: add tests
* feat/173: OSC API
* feat/173: Websocket API
2022-07-13 15:49:59 +02:00
Carlos Valente 91a0d975b3 lint/css config stylelint (#176) 2022-07-11 22:56:35 +02:00
Carlos Valente 97e48fc56e Hotfix/countdown delay (#175)
* hotfix/countdown-delay: fix styling on overran event
* hotfix/countdown-delay: prevent falsy issue
* hotfix/countdown-delay: show delay in countdown
* hotfix/countdown-delay: version bump
2022-07-11 22:04:56 +02:00
Carlos Valente 5c3e9e6a2e hotfix/network: increase network timeout (#170) 2022-07-01 08:02:53 +02:00
Carlos Valente 8ece6b141c hotfix/167: style negative timers (#169) 2022-07-01 00:03:26 +02:00
Carlos Valente 3edb534e90 Hotfix/docker (#168)
* hotfix/docker: missing comma
2022-06-30 23:51:24 +02:00
Carlos Valente a9ee0c3f1d feat/120: prevent reflow (#166) 2022-06-30 23:34:23 +02:00
Carlos Valente f4c5a2bf87 Feat/120 (#164)
* feat/120: calculate delay to single event
* feat/120 Countdown
* feat/120 prevent parsing NaN
* feat/120: version bump
* feat/120: update README.md
* feat/120: show delayed countdown timer
2022-06-30 20:26:01 +02:00
Carlos Valente dcc534f92b add timeout to test CI 2022-06-16 22:44:46 +02:00
Carlos Valente 2ee4986dcf Update build.yml
add timeout to build steps
2022-06-16 22:43:54 +02:00
Carlos Valente c1de3190e9 Fix/155 (#158)
* fix/155: fix css polution on detached views
* fix/155: test detached routes
2022-06-16 14:27:13 +02:00
Carlos Valente 1f001cf188 Feat/153 (#157)
* feat/153: always roll
* feat/153: version bump
2022-06-15 20:37:21 +02:00
Carlos Valente 323d365f1a Fix/147 (#152)
* fix/147: add latest tag to release
* fix/147: add latest tag to docker compose
* fix/148: add timezone example to docker-compose
* docker: version bump
2022-06-13 22:11:17 +02:00
Carlos Valente dda92ba3a8 Refactor imports
* refactor: implement import lint
2022-06-11 23:17:16 +02:00
Carlos Valente c576b6308e Update build.yml 2022-06-08 22:22:03 +02:00
Carlos Valente 5802e7cd03 Update build.yml 2022-06-08 22:14:38 +02:00
Carlos Valente 86957593bb Update build.yml 2022-06-08 22:03:28 +02:00
Carlos Valente 9c5c7ee69a Docker (#146)
* docker: initial config

* docker: add entrypoint for headless run

* docker: resolve path from env variable

* docker: initial config

* docker: rename preloaded data directories

* docker: update README

* docker: prevent issue with port mappings

* docker: update README.md

* docker: add docker-compose

* docker: cleanup logs

* docker: add CI

* docker: cleanup unused

* docker: use static port in development

* refactor: isolate tasks

* docker: add demo db

* fix: download db resolves according to OS

* docker: config build

* fix: ensure db path exists

* fix: add object type to block

* docker: config build

* docker: config build

* docker: env is production

* docker: preload db

* fix: remove test dir

* docker: graceful shutdown node app

* docker: env is production

* docker: add docker-compose

* docker: remove unused mappings

* docker: version bump
2022-06-08 21:57:22 +02:00
Carlos Valente 94a7ef6318 fix/141: enable applying negative delays (#142) 2022-06-07 18:57:44 +02:00
Carlos Valente c394cd5d4b Feat/104 (#140)
* feat/104: isolate control components
* feat/104: open components in isolated window
* feat/104: update tests
2022-05-29 11:19:29 +02:00
Carlos Valente 412d230cee Ux/134 (#139)
* ui/134: highlight active messages
* ui/134: create theming template
* ui/134: version bump
2022-05-28 16:25:56 +02:00
Carlos Valente 8c2d6595b0 feat/117: add toggle fullscreen button (#138) 2022-05-28 15:57:59 +02:00
Carlos Valente ca33624ffc Update README.md
add comments on linux install
2022-05-22 10:21:43 +02:00
291 changed files with 9282 additions and 5015 deletions
+14
View File
@@ -0,0 +1,14 @@
# Ignore config files
.prettier*
# Ignore install stuff
**/yarn.lock
**/yarn-error.log
# Ignore test files
**/__tests__/**
**/**.test.js
# Ignore git and cache folders
.git
.cache
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

-103
View File
@@ -1,103 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
push:
# Pattern matched against refs/tags
tags:
# Push events to every tag not containing /
- '*'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# build a file for windows
buildwin:
# The type of runner that the job will run on
runs-on: windows-latest
env:
CI: ''
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
# install and build react
- name: Install React dependencies
run: yarn install
working-directory: ./client
- name: Build React App
run: yarn build
working-directory: ./client
# install node dependencies
- name: Install nodejs dependencies
run: yarn install
working-directory: ./server/src
# install and build electron
- name: Install Electron dependencies
run: yarn install
working-directory: ./server
- name: Build Electron App
run: yarn dist-win
working-directory: ./server
# release
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ./server/dist/ontime-win64.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# build a file for mac
# buildmac:
# The type of runner that the job will run on
# runs-on: macOS-latest
# env:
# CI: ''
# Steps represent a sequence of tasks that will be executed as part of the job
# steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
# - uses: actions/checkout@v2
# - name: Use Node.js
# uses: actions/setup-node@v1
# with:
# node-version: '14.x'
# install and build react
# - name: Install React dependencies
# run: yarn install
# working-directory: ./client
# - name: Build React App
# run: yarn build
# working-directory: ./client
# install and build electron
# - name: Install Electron + nodejs dependencies
# run: yarn install
# working-directory: ./server
# - name: Build Electron App
# run: yarn dist-win
# 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 }}
+61 -9
View File
@@ -10,6 +10,7 @@ on:
jobs:
build_mac:
runs-on: macOS-latest
timeout-minutes: 20
env:
CI: ''
@@ -22,7 +23,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
@@ -31,13 +32,13 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install && yarn setdb
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-mac
@@ -53,6 +54,7 @@ jobs:
build_win:
runs-on: windows-latest
timeout-minutes: 20
env:
CI: ''
@@ -65,7 +67,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
@@ -74,13 +76,13 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install && yarn setdb
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-win
@@ -96,6 +98,7 @@ jobs:
build_linux:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
@@ -108,7 +111,7 @@ jobs:
# React
- name: React - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
@@ -117,13 +120,13 @@ jobs:
# Node server
- name: Server - Install dependencies
run: yarn install
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install && yarn setdb
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-linux
@@ -136,3 +139,52 @@ jobs:
files: ./server/dist/ontime-linux.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish_docker:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Setup env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
# React
- name: React - Install dependencies
run: yarn install --network-timeout 300000
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# Login to docker
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Prepare builder
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
# Build and push
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
+2 -1
View File
@@ -9,6 +9,7 @@ jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
@@ -50,4 +51,4 @@ jobs:
uses: cypress-io/github-action@v2
with:
working-directory: ./server
start: yarn cypress
start: yarn cypress
+3 -1
View File
@@ -28,9 +28,11 @@ yarn-error.log*
# working stuff
_SS/
db backup.json
server/src/data/db.json
server/src/preloaded-db/db.json
server/src/models/db.json
TODO.md
ontime-db/
ontime-external/
# vscode stuff
.vscode/*
+1
View File
@@ -46,5 +46,6 @@
</list>
</option>
</inspection_tool>
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>
+20
View File
@@ -0,0 +1,20 @@
FROM node:14-alpine
WORKDIR /app/server
# Prepare UI
COPY /client/build ../client/build
# Prepare Backend
COPY /server/src ./
# Export default ports Main - OSC IN
EXPOSE 4001/tcp 8888/udp
ENV NODE_ENV=production
ENV ONTIME_DATA=/server/
CMD ["yarn", "start:headless"]
# Build an run commandsN
# docker build -t getontime/ontime .
# docker run -p 4001:4001 -p 10.0.0.12:8888:8888/udp --mount type=bind,source="$(pwd)/ontime-db",target=/server/preloaded-db getontime/ontime
+56 -11
View File
@@ -7,6 +7,7 @@
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/mac-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/win-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/></a>
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/dockerhub.png"/></a>
</div>
# Ontime
@@ -65,19 +66,24 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
- Public Info
- Picture in Picture
- Studio Clock
- [Make your own?](#make-your-own-viewer)
- [Make your own?](#make-your-own-viewer)
- [x] Configurable realtime Lower Thirds
- [x] Cuesheets with additional custom fields
- [x] Send live messages to different screen types
- [x] Ability to differentiate between backstage and public data
- [x] Manage delays workflow
- [x] Open Sound Control (OSC) Control and Feedback
- [x] Integrate with hardware using Companion or one of the APIs
- [x] Roll mode: run independently using the system clock
- [x] Import event list from Excel
- [x] URL Aliases (define configurable aliases to ease onsite setup)
- [x] Logging view
- [x] Edit anywhere: run ontime in your local network and use any machine to reach the editor page (
same as app)
- [x] Multi platform (available on Windows, MacOS and Linux)
- [x] [Headless run](#headless-run) (run server only, configure from a browser locally)
- [x] [Countdown to anything!](https://cpvalente.gitbook.io/ontime/views/countdown): ability to have
a countdown to any scheduled event
## Unopinionated
@@ -104,7 +110,9 @@ Taking advantage of the integrations in Ontime, we currently use Ontime with:
webmodule
- `OBS`: **render views** using the Browser Module
- `QLab`: trigger ontime using **OSC API**
- `Companion`: trigger ontime and manipulate timer using **OSC API**
- `Companion`: Ontime has a **companion module**. Issue report and feature requests should be done
in
the [repository getontime/ontime](https://github.com/bitfocus/companion-module-getontime-ontime)
### Make your own viewer
@@ -115,6 +123,33 @@ See [this repository](https://github.com/cpvalente/ontime-viewer-template) with
how to get you started and read the docs about
the [Websocket API](https://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api)
### Headless run
You can self host and run ontime in a docker image, the run command should:
- expose the necessary ports (listen in Dockerfile)
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
- the image name __getontime/ontime__
The docker image is
in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
```bash
docker pull getontime/ontime
```
```bash
# Port 4001 - ontime server port
# Port 8888 - OSC input, bound to localhost IP Address
docker run -p 4001:4001 -p 127.0.0.1:8888:8888/udp --mount type=bind,source="$(pwd)/ontime-db",target=/server/preloaded-db getontime/ontime
```
or if running from the docker compose
```bash
docker-compose up
```
## Roadmap
### Continued development
@@ -123,28 +158,38 @@ There are several features planned in the roadmap. These will be implemented in
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)
- [ ] Headless version (run server only anywhere, configure from a browser locally)
- [ ] Companion module
- [ ] Improvements in event interface
- [ ] Moderator view
- [ ] New playback mode for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
- [ ] Lower Third Manager
- [ ] Note only event
- [ ] Reach Schedule: way to speedup timer to meet a deadline
### Issues
The app is still in pre-release and there are a few issues, mainly concerning responsiveness in
different screens. If you run into problems, please open an issue with a screenshot and note your
device and its screen resolution
If you come across any bugs, [please open an issue]((https://github.com/cpvalente/ontime/issues/new)). Usually bugs get fixed pretty quickly when reported
#### 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```
or in Linux
```Could Not Display "ontime-linux.AppImage```
You can circumvent this by allowing the execution of the app manually.
- In Windows: click more and select "Run Anyway"
- in macOS: after attempting to run the installer, navigate to System Preferences -> Security &
Privacy and allow the execution of the app
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing
File as a Program
Long story short: Ontime app is unsigned. </br>Purchasing the certificates for both Mac and Windows
would mean a recurrent expense and is not a priority. This is unlikely to change in future. If you
have tips on how to improve this, or would like to sponsor the code signing,
+2
View File
@@ -0,0 +1,2 @@
node_modules
build
+56 -3
View File
@@ -7,7 +7,7 @@
"plugins": [
"react",
"testing-library",
"jest"
"simple-import-sort"
],
"rules": {
"jest/no-mocks-import": "warn",
@@ -41,6 +41,8 @@
"react/no-deprecated": [
"error"
],
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-no-undef": "error",
"react/jsx-closing-tag-location": "warn",
"react/jsx-no-useless-fragment": "warn",
@@ -77,6 +79,57 @@
"customValidators": [],
"skipUndeclared": true
}
]
}
],
"simple-import-sort/imports": "warn",
"simple-import-sort/exports": "warn",
"import/no-duplicates": "error"
},
"overrides": [
// override "simple-import-sort" config
{
"files": [
"*.js",
"*.jsx",
"*.ts",
"*.tsx"
],
"rules": {
"simple-import-sort/imports": [
"error",
{
"groups": [
// Packages `react` related packages come first.
[
"^react",
"^@?\\w"
],
// Internal packages.
[
"^(@|components)(/.*|$)"
],
// Side effect imports.
[
"^\\u0000"
],
// Parent imports. Put `..` last.
[
"^\\.\\.(?!/?$)",
"^\\.\\./?$"
],
// Other relative imports. Put same-folder imports and `.` last.
[
"^\\./(?=.*/)(?!/?$)",
"^\\.(?!/?$)",
"^\\./?$"
],
// Style imports.
[
"^.+\\.?(css)$"
]
]
}
]
}
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-prettier"
]
}
-6
View File
@@ -1,6 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
+28 -12
View File
@@ -1,28 +1,31 @@
{
"name": "ontime-ui",
"version": "1.0.1",
"version": "1.9.0",
"private": true,
"dependencies": {
"@chakra-ui/react": "^2.0.0-next.3",
"@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",
"@chakra-ui/react": "^2.3.2",
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@react-icons/all-files": "^4.1.0",
"@tanstack/react-query": "^4.1.3",
"@tanstack/react-query-devtools": "^4.0.10",
"autosize": "^5.0.1",
"axios": "^0.25.0",
"axios": "^0.27.2",
"color": "^4.2.3",
"framer-motion": "^6.3.3",
"framer-motion": "^7.3.2",
"jotai": "^1.7.8",
"luxon": "^3.0.1",
"react": "^18.1.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.1.0",
"react-fast-compare": "^3.2.0",
"react-qr-code": "^2.0.5",
"react-query": "^3.38.0",
"react-router-dom": "^6.3.0",
"react-table": "^7.7.0",
"socket.io-client": "^4.5.0",
"socket.io-client": "^4.5.1",
"typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^2.1.4"
@@ -30,6 +33,8 @@
"scripts": {
"start": "set BROWSER=none&&react-scripts start",
"build": "react-scripts build",
"lint": "eslint .",
"stylelint": "npx stylelint \"**/*.scss\"\n",
"test": "react-scripts test",
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
"eject": "react-scripts eject"
@@ -51,9 +56,20 @@
"@testing-library/react": "^13.1.1",
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^14.1.1",
"@types/jest": "^29.0.1",
"@types/node": "^18.7.16",
"@types/react": "^18.0.19",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"prop-types": "^15.8.1",
"react-scripts": "^5.0.1",
"react-test-renderer": "^18.1.0",
"sass": "^1.51.0"
"sass": "^1.51.0",
"stylelint": "^14.9.1",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard-scss": "^4.0.0",
"typescript": "^4.8.3"
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#121212" />
<meta name="onsite" content="onsite - event timer manager" />
<meta name="ontime" content="ontime - event timer manager" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link
rel="icon"
+1
View File
@@ -0,0 +1 @@
/// <reference types="react-scripts" />
+31 -73
View File
@@ -1,36 +1,20 @@
import React, { lazy, Suspense, useCallback, useEffect } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import './App.scss';
import withSocket from 'features/viewers/ViewWrapper';
import { Suspense, useCallback, useEffect } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import { useFetch } from './app/hooks/useFetch';
import { ALIASES } from './app/api/apiConstants';
import { getAliases } from './app/api/ontimeApi';
const Editor = lazy(() => import('features/editors/ProtectedEditor'));
const Table = lazy(() => import('features/table/ProtectedTable'));
import { AppContextProvider } from './common/context/AppContext';
import SocketProvider from './common/context/socketContext';
import theme from './theme/theme';
import AppRouter from './AppRouter';
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer'));
const StageManager = lazy(() => import('features/viewers/backstage/StageManager'));
const Public = lazy(() => import('features/viewers/foh/Public'));
const Lower = lazy(() => import('features/viewers/production/lower/LowerWrapper'));
const Pip = lazy(() => import('features/viewers/production/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
const STimer = withSocket(TimerView);
const SMinimalTimer = withSocket(MinimalTimerView);
const SStageManager = withSocket(StageManager);
const SPublic = withSocket(Public);
const SLowerThird = withSocket(Lower);
const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
// Load Open Sans typeface
require('typeface-open-sans');
export const ontimeQueryClient = new QueryClient();
function App() {
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
const navigate = useNavigate();
// Handle keyboard shortcuts
const handleKeyPress = useCallback((e) => {
@@ -58,53 +42,27 @@ function App() {
};
}, [handleKeyPress]);
// navigate if is alias route
useEffect(() => {
if (data == null) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
break;
}
}
}, [data, location, navigate]);
return (
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<Routes>
<Route path='/' element={<STimer />} />
<Route path='/speaker' element={<STimer />} />
<Route path='/presenter' element={<STimer />} />
<Route path='/stage' element={<STimer />} />
<Route path='/timer' element={<STimer />} />
<Route path='/minimal' element={<SMinimalTimer />} />
<Route path='/minimalTimer' element={<SMinimalTimer />} />
<Route path='/simpleTimer' element={<SMinimalTimer />} />
<Route path='/sm' element={<SStageManager />} />
<Route path='/backstage' element={<SStageManager />} />
<Route path='/public' element={<SPublic />} />
<Route path='/pip' element={<SPip />} />
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
{/*/!* Protected Routes *!/*/}
<Route path='/editor' element={<Editor />} />
<Route path='/cuesheet' element={<Table />} />
<Route path='/cuelist' element={<Table />} />
<Route path='/table' element={<Table />} />
{/* Send to default if nothing found */}
<Route path='*' element={<STimer />} />
</Routes>
</Suspense>
</ErrorBoundary>
</div>
<ChakraProvider resetCSS theme={theme}>
<SocketProvider>
<QueryClientProvider client={ontimeQueryClient}>
<AppContextProvider>
<BrowserRouter>
<div className='App'>
<ErrorBoundary>
<Suspense fallback={null}>
<AppRouter />
</Suspense>
</ErrorBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</div>
</BrowserRouter>
</AppContextProvider>
</QueryClientProvider>
</SocketProvider>
</ChakraProvider>
);
}
-45
View File
@@ -1,45 +0,0 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scrollbar-color: rgba(255, 255, 255, 0.15) rgba(255, 255, 255, 0.01);
scrollbar-width: thin;
}
body,
html,
.App {
margin: 0 auto;
overflow: hidden;
overflow: clip;
height: 100vh;
-webkit-user-select: none;
user-select: none;
-webkit-app-region: drag;
}
option {
color: black;
}
/* width */
::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
}
/* Track */
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.01);
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.35);
}
+125
View File
@@ -0,0 +1,125 @@
import { lazy, useEffect } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { ALIASES } from './common/api/apiConstants';
import { getAliases } from './common/api/ontimeApi';
import { useFetch } from './common/hooks/useFetch';
import { useSocketProvider } from './common/hooks/useSocketProvider';
import withSocket from './features/viewers/ViewWrapper';
const Editor = lazy(() => import('features/editors/ProtectedEditor'));
const Table = lazy(() => import('features/table/ProtectedTable'));
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
const MinimalTimerView = lazy(() => import('features/viewers/minimal-timer/MinimalTimer'));
const ClockView = lazy(() => import('features/viewers/clock/Clock'));
const Countdown = lazy(() => import('features/viewers/countdown/Countdown'));
const Backstage = lazy(() => import('features/viewers/backstage/Backstage'));
const Public = lazy(() => import('features/viewers/public/Public'));
const Lower = lazy(() => import('features/viewers/lower-thirds/LowerWrapper'));
const Pip = lazy(() => import('features/viewers/picture-in-picture/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
const STimer = withSocket(TimerView);
const SMinimalTimer = withSocket(MinimalTimerView);
const SClock = withSocket(ClockView);
const SCountdown = withSocket(Countdown);
const SBackstage = withSocket(Backstage);
const SPublic = withSocket(Public);
const SLowerThird = withSocket(Lower);
const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
const FeatureWrapper = lazy(() => import('features/FeatureWrapper'));
const EventList = lazy(() => import('features/editors/list/EventListExport'));
const TimerControl = lazy(() => import('features/control/playback/TimerControlExport'));
const MessageControl = lazy(() => import('features/control/message/MessageControlExport'));
const Info = lazy(() => import('features/info/InfoExport'));
export default function AppRouter() {
useSocketProvider();
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
const navigate = useNavigate();
// navigate if is alias route
useEffect(() => {
if (!data) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
break;
}
}
}, [data, location, navigate]);
return(
<Routes>
<Route path='/' element={<STimer />} />
<Route path='/speaker' element={<STimer />} />
<Route path='/presenter' element={<STimer />} />
<Route path='/stage' element={<STimer />} />
<Route path='/timer' element={<STimer />} />
<Route path='/minimal' element={<SMinimalTimer />} />
<Route path='/minimalTimer' element={<SMinimalTimer />} />
<Route path='/simpleTimer' element={<SMinimalTimer />} />
<Route path='/clock' element={<SClock />} />
<Route path='/countdown' element={<SCountdown />} />
<Route path='/sm' element={<SBackstage />} />
<Route path='/backstage' element={<SBackstage />} />
<Route path='/public' element={<SPublic />} />
<Route path='/pip' element={<SPip />} />
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
{/*/!* Protected Routes *!/*/}
<Route path='/editor' element={<Editor />} />
<Route path='/cuesheet' element={<Table />} />
<Route path='/cuelist' element={<Table />} />
<Route path='/table' element={<Table />} />
{/*/!* Protected Routes - Elements *!/*/}
<Route
path='/eventlist'
element={
<FeatureWrapper>
<EventList />
</FeatureWrapper>
}
/>
<Route
path='/timercontrol'
element={
<FeatureWrapper>
<TimerControl />
</FeatureWrapper>
}
/>
<Route
path='/messagecontrol'
element={
<FeatureWrapper>
<MessageControl />
</FeatureWrapper>
}
/>
<Route
path='/info'
element={
<FeatureWrapper>
<Info />
</FeatureWrapper>
}
/>
{/* Send to default if nothing found */}
<Route path='*' element={<STimer />} />
</Routes>
)
}
+2 -2
View File
@@ -1,4 +1,4 @@
import {QueryClient} from "react-query";
import { QueryClient } from '@tanstack/react-query';
export const queryClientMock = new QueryClient({
defaultOptions: {
@@ -6,4 +6,4 @@ export const queryClientMock = new QueryClient({
retry: false,
},
},
});
});
-22
View File
@@ -1,22 +0,0 @@
export const NODE_PORT = 4001;
export const EVENT_TABLE = 'event';
export const ALIASES = 'aliases';
export const USERFIELDS = 'userFields';
export const EVENTS_TABLE = 'events';
export const APP_TABLE = 'appinfo';
export const OSC_SETTINGS = 'oscSettings';
export const APP_SETTINGS = 'appSettings';
/**
* @description finds server path given the current location
* @return {*}
*/
const calculateServer = () => {
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
};
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`;
@@ -1,32 +0,0 @@
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>
);
};
-23
View File
@@ -1,23 +0,0 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import io from 'socket.io-client';
import { serverURL } from 'app/api/apiConstants';
const SocketContext = createContext([[], () => {}]);
export const useSocket = () => {
return useContext(SocketContext);
};
function SocketProvider({ children }) {
const [socket, setSocket] = useState();
useEffect(() => {
const s = io(serverURL, { transports: ['websocket'] });
setSocket(s);
return () => s.disconnect();
}, []);
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
}
export default SocketProvider;
-52
View File
@@ -1,52 +0,0 @@
const dummy = new Date();
export const sampleData = {
presenterMessage: {
text: 'Only the timer sees this',
active: false,
},
publicMessage: {
text: 'Everyone sees this',
active: false,
},
events: [
{
id: '1',
title: 'Is the internet a fad?',
subtitle: 'It is',
presenter: 'Carlos Valente',
timeStart: 37800000,
timeEnd: 39600000,
clockStarted: dummy,
timerDuration: 60,
type: 'event',
},
{
id: 0.4849093424577693,
duration: 25*60000,
type: 'delay',
},
{
id: '2',
title: 'Is reddit a dictatorship?',
subtitle: 'It is',
presenter: 'Carlos Valente',
timeStart: 39600000,
timeEnd: 40320000,
clockStarted: dummy,
timerDuration: 60*60000,
type: 'event',
},
{
id: '3',
title: 'Out of words',
subtitle: '',
presenter: 'Carlos Valente',
timeStart: 41400000,
timeEnd: 43200000,
clockStarted: dummy,
timerDuration: 60,
type: 'event',
},
],
};
@@ -1,4 +1,5 @@
// Exported viewer link location
const minimalLocation = 'minimal';
const speakerLocation = 'speaker';
const smLocation = 'sm';
const publicLocation = 'public';
@@ -7,6 +8,7 @@ const studioLocation = 'studio';
const cuesheetLocation = 'cuesheet';
export const viewerLocations = [
{ link: minimalLocation, label: 'Minimal timer' },
{ link: speakerLocation, label: 'Speaker Screen' },
{ link: smLocation, label: 'Backstage Screen' },
{ link: publicLocation, label: 'Public Screen' },
+36
View File
@@ -0,0 +1,36 @@
export const STATIC_PORT = 4001;
export const EVENT_TABLE = ['event'];
export const ALIASES = ['aliases'];
export const USERFIELDS = ['userFields'];
export const EVENTS_TABLE = ['events'];
export const APP_TABLE = ['appinfo'];
export const OSC_SETTINGS = ['oscSettings'];
export const APP_SETTINGS = ['appSettings'];
export const VIEW_SETTINGS = ['viewSettings'];
export const FEAT_EVENTLIST = ['feat-eventList'];
export const FEAT_MESSAGECONTROL = ['feat-messagecontrol'];
export const FEAT_PLAYBACKCONTROL = ['feat-playbackcontrol'];
export const FEAT_INFO = ['feat-info'];
export const FEAT_CUESHEET = ['feat-cuesheet'];
export const TIMER = ['ontime-timer'];
/**
* @description finds server path given the current location, it
* @return {*}
*/
export const calculateServer = () => {
if (process.env?.NODE_ENV === 'development') {
return `http://localhost:${STATIC_PORT}`;
}
return window.location.origin;
};
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 stylesPath = 'external/styles/override.css';
export const overrideStylesURL = `serverURL/${stylesPath}`;
@@ -1,4 +1,5 @@
import axios from 'axios';
import { eventURL } from './apiConstants';
/**
@@ -1,4 +1,5 @@
import axios from 'axios';
import { eventsURL } from './apiConstants';
/**
@@ -1,4 +1,5 @@
import axios from 'axios';
import { ontimeURL } from './apiConstants';
/**
@@ -18,7 +19,12 @@ export const ontimePlaceholderInfo = {
* @type {{pinCode: null}}
*/
export const ontimePlaceholderSettings = {
app: 'ontime',
version: 1,
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
};
/**
@@ -61,6 +67,14 @@ export const oscPlaceholderSettings = {
enabled: false,
};
/**
* @description placeholder information for view settings
* @type {{overrideCSS: boolean}}
*/
export const viewsPlaceholder = {
overrideStyles: false,
};
/**
* @description placeholder information for httpSettings
* @type {{onStart: {url: string, enabled: boolean}, onLoad: {url: string, enabled: boolean}, onPause: {url: string, enabled: boolean}, onFinish: {url: string, enabled: boolean}, onUpdate: {url: string, enabled: boolean}, onStop: {url: string, enabled: boolean}}}
@@ -157,6 +171,21 @@ export const getInfo = async () => {
*/
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
/**
* @description HTTP request to retrieve view settings
* @return {Promise}
*/
export const getView = async () => {
const res = await axios.get(`${ontimeURL}/views`);
return res.data;
};
/**
* @description HTTP request to mutate view settings
* @return {Promise}
*/
export const postView = async (data) => axios.post(`${ontimeURL}/views`, data);
/**
* @description HTTP request to retrieve aliases
* @return {Promise}
@@ -222,7 +251,7 @@ export const downloadEvents = async () => {
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
const url = window.URL.createObjectURL(new Blob([response.data]));
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
@@ -235,18 +264,26 @@ export const downloadEvents = async () => {
* @description HTTP request to upload events db
* @return {Promise}
*/
export const uploadEvents = async (file) => {
export const uploadEvents = async (file, setProgress, options) => {
const formData = new FormData();
formData.append('userFile', file); // appending file
await axios.post(`${ontimeURL}/db`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
formData.append('userFile', file);
const onlyEvents = options?.onlyEvents;
await axios
.post(`${ontimeURL}/db?onlyEvents=${onlyEvents}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
const complete = Math.round((progressEvent.loaded * 100) / progressEvent.total);
setProgress(complete);
},
})
.then((response) => response.data.id);
};
/**
* @description HTTP request to upload events
* @return {Promise}
*/
export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath });
export const uploadEventsWithPath = async (filepath) =>
axios.post(`${ontimeURL}/dbpath`, { path: filepath });
@@ -1,4 +1,5 @@
import axios from 'axios';
import { playbackURL } from './apiConstants';
/**
@@ -0,0 +1,20 @@
import { atomWithStorage, selectAtom } from 'jotai/utils';
export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', {
showQuickEntry: false,
startTimeIsLastEnd: false,
defaultPublic: false,
});
export const showQuickEntryAtom = selectAtom(
eventSettingsAtom,
(settings) => settings.showQuickEntry
);
export const startTimeIsLastEndAtom = selectAtom(
eventSettingsAtom,
(settings) => settings.startTimeIsLastEnd
);
export const defaultPublicAtom = selectAtom(
eventSettingsAtom,
(settings) => settings.defaultPublic
);
@@ -1,12 +1,13 @@
import React from 'react';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { IconButton } from '@chakra-ui/button';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function ActionButtons(props) {
const { showAdd, showDelay, showBlock, actionHandler } = props;
@@ -17,7 +18,7 @@ export default function ActionButtons(props) {
return (
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add ...' delay={500}>
<Tooltip label='Add ...' delay={tooltipDelayMid}>
<MenuButton
as={IconButton}
aria-label='Options'
@@ -1,7 +1,6 @@
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';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import PropTypes from 'prop-types';
export default function EnableBtn(props) {
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function PauseIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label='Pause timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPause size='24px' />}
colorScheme='orange'
@@ -1,4 +1,3 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Button, IconButton } from '@chakra-ui/button';
import {
AlertDialog,
@@ -11,7 +11,8 @@ import {
import { Tooltip } from '@chakra-ui/tooltip';
import { FiPower } from '@react-icons/all-files/fi/FiPower';
import PropTypes from 'prop-types';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { LoggingContext } from '../../context/LoggingContext';
export default function QuitIconBtn(props) {
const { clickHandler, size = 'lg', ...rest } = props;
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function RollIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label='Roll mode' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoTimeOutline size='24px' />}
colorScheme='blue'
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function StartIconBtn(props) {
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label='Start timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlay size='24px' />}
colorScheme='green'
@@ -1,27 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
export default function TooltipActionBtn(props) {
const { clickHandler, icon, color, size='xs', tooltip, ...rest } = props;
return (
<Tooltip label={tooltip}>
<IconButton
aria-label={tooltip}
size={size}
icon={icon}
onClick={clickHandler}
{...rest}
/>
</Tooltip>
);
}
TooltipActionBtn.propTypes = {
clickHandler: PropTypes.func,
icon: PropTypes.element,
color: PropTypes.string,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
tooltip: PropTypes.string
}
@@ -0,0 +1,27 @@
import { IconButton } from '@chakra-ui/button';
import { IconButtonProps } from '@chakra-ui/react';
import { Tooltip } from '@chakra-ui/tooltip';
export type Sizes = 'xs' | 'sm' | 'md' | 'lg';
interface TooltipActionBtnProps extends IconButtonProps {
clickHandler: () => void;
tooltip: string;
openDelay?: number;
}
export default function TooltipActionBtn(props: TooltipActionBtnProps) {
const { clickHandler, icon, size = 'xs', tooltip, openDelay = 0, className, ...rest } = props;
return (
<Tooltip label={tooltip} openDelay={openDelay}>
<IconButton
{...rest}
aria-label={tooltip}
size={size}
icon={icon}
onClick={clickHandler}
className={className}
/>
</Tooltip>
);
}
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import { useCallback, useState } from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
@@ -1,12 +1,13 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function TransportIconBtn(props) {
const { clickHandler, icon, tooltip, disabled, ...rest } = props;
return (
<Tooltip label={tooltip} openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label={tooltip} openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={icon}
colorScheme='white'
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../ontimeConfig';
export default function UnloadIconBtn(props) {
const { clickHandler, disabled, ...rest } = props;
return (
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
<Tooltip label='Unload event' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={<IoStop size='22px' />}
colorScheme='red'
@@ -1,7 +1,7 @@
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 PropTypes from 'prop-types';
import style from './CollapseBar.module.scss';
export default function CollapseBar(props) {
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.header,
.headerRoll {
@@ -1,28 +0,0 @@
import React, { memo } from 'react';
import { formatDisplay } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import styles from './Countdown.module.scss';
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
// prepare display string
const display =
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
const classes = `${small ? styles.countdownClockSmall : styles.countdownClock}
${isNegative ? styles.negative : ''}`;
return (
<div className={classes}>
{display}
</div>
);
};
export default memo(Countdown);
Countdown.propTypes = {
time: PropTypes.number,
small: PropTypes.bool,
isNegative: PropTypes.bool,
hideZeroHours: PropTypes.bool,
};
@@ -1,29 +0,0 @@
@use '../../../styles/main' as *;
/* Common */
.countdownClock,
.countdownClockSmall {
font-family: 'Open Sans', sans-serif;
line-height: 0.9;
color: $text-white;
}
/* Viewers */
.countdownClock {
font-size: 21vw;
line-height: 21vw;
text-align: center;
letter-spacing: 1vw;
}
/* Editor */
.countdownClockSmall {
font-size: 4em;
text-align: center;
letter-spacing: 0.1em;
font-weight: 600;
}
.negative {
color: $ontime-pink;
}
@@ -0,0 +1,26 @@
import { memo } from 'react';
import { formatDisplay } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import './TimerDisplay.scss';
const TimerDisplay = ({ time, small, isNegative, hideZeroHours }) => {
const display =
(time === null || typeof time === 'undefined' || isNaN(time))
? '-- : -- : --'
: formatDisplay(time, hideZeroHours);
const classes = `timer ${small ? 'timer--small' : ''} ${isNegative ? 'timer--finished' : ''}`;
return <div className={classes}>{display}</div>;
};
export default memo(TimerDisplay);
TimerDisplay.propTypes = {
time: PropTypes.number,
small: PropTypes.bool,
isNegative: PropTypes.bool,
hideZeroHours: PropTypes.bool,
};
@@ -0,0 +1,22 @@
@use '../../../theme/viewerDefs' as *;
.timer {
font-family: 'Open Sans', sans-serif;
color: var(--timer-color-override, $viewer-color);
font-size: 21vw;
line-height: 21vw;
text-align: center;
letter-spacing: 1vw;
&--small {
font-size: 4em;
line-height: 0.9;
text-align: center;
letter-spacing: 0.1em;
font-weight: 600;
}
&--finished {
color: $timer-finished-color;
}
}
@@ -1,7 +1,10 @@
/* eslint-disable react/destructuring-assignment */
import React from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { LoggingContext } from '../../context/LoggingContext';
import style from './ErrorBoundary.module.scss';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const appVersion = require('../../../../package.json').version;
class ErrorBoundary extends React.Component {
@@ -25,8 +28,8 @@ class ErrorBoundary extends React.Component {
});
try {
this.context.emitError(error.toString());
} catch {
console.log('Unable to emit error')
} catch (e) {
console.log('Unable to emit error', error, e )
}
this.reportContent = `${error} ${info.componentStack}`;
}
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.errorContainer {
width: 100%;
@@ -1,9 +1,10 @@
import React, { useCallback, useContext } from 'react';
import EditableTimer from 'common/input/EditableTimer';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import { useCallback, useContext } from 'react';
import EditableTimer from 'common/components/input/EditableTimer';
import PropTypes from 'prop-types';
import { LoggingContext } from '../../context/LoggingContext';
import { validateTimes } from '../../utils/entryValidator';
export default function EventTimes(props) {
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
const { emitWarning } = useContext(LoggingContext);
@@ -1,9 +1,11 @@
import React, { useCallback, useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import { useCallback, useContext } from 'react';
import PropTypes from 'prop-types';
import TimesDelayed from './TimesDelayed';
import { LoggingContext } from '../../context/LoggingContext';
import { validateTimes } from '../../utils/entryValidator';
import Times from './Times';
import TimesDelayed from './TimesDelayed';
export default function EventTimesVertical(props) {
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
@@ -1,7 +1,8 @@
import React from 'react';
import EditableTimer from '../../input/EditableTimer';
import PropTypes from 'prop-types';
import style from './Times.module.scss'
import EditableTimer from '../input/EditableTimer';
import style from './Times.module.scss';
export default function Times(props) {
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
@@ -37,7 +38,7 @@ export default function Times(props) {
/>
</>
);
};
}
Times.propTypes = {
handleValidate: PropTypes.func.isRequired,
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.label {
font-size: 0.75em;
@@ -1,8 +1,9 @@
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'
import EditableTimer from '../input/EditableTimer';
import style from './Times.module.scss';
export default function TimesDelayed(props) {
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
@@ -45,7 +46,7 @@ export default function TimesDelayed(props) {
/>
</>
);
};
}
TimesDelayed.propTypes = {
handleValidate: PropTypes.func.isRequired,
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { Textarea } from '@chakra-ui/react';
import autosize from 'autosize/dist/autosize';
@@ -1,9 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
import { clamp } from '../../app/utils/math';
import style from './TimeInput.module.css';
import PropTypes from 'prop-types';
import { clamp } from '../../utils/math';
import style from './TimeInput.module.css';
const inputProps = {
width: 20,
fontWeight: 400,
@@ -1,8 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import style from './EditableText.module.scss';
import PropTypes from 'prop-types';
import style from './EditableText.module.scss';
export default function EditableText(props) {
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
const [text, setText] = useState(defaultValue || '');
@@ -1,4 +1,4 @@
@use '../../styles/main' as *;
@use '../../../theme/main' as *;
.block {
overflow: hidden;
@@ -1,9 +1,11 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { forgivingStringToMillis } from '../utils/dateConfig';
import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types';
import { stringFromMillis } from '../utils/time';
import { LoggingContext } from '../../context/LoggingContext';
import { forgivingStringToMillis } from '../../utils/dateConfig';
import { stringFromMillis } from '../../utils/time';
import style from './EditableTimer.module.scss';
export default function EditableTimer(props) {
@@ -1,4 +1,4 @@
@use '../../styles/main'as *;
@use '../../../theme/main'as *;
.editable,
.delayedEditable {
@@ -1,6 +1,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import EditableTimer from '../EditableTimer';
const renderEditableTimer = (
@@ -1,6 +1,6 @@
import React from 'react';
import { clamp } from 'app/utils/math';
import { clamp } from 'common/utils/math';
import PropTypes from 'prop-types';
import styles from './MyProgressBar.module.scss';
export default function MyProgressBar(props) {
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.progress,
.progressCountdown {
+50 -30
View File
@@ -1,14 +1,31 @@
import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { IconButton } from '@chakra-ui/button';
import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { IoContract } from '@react-icons/all-files/io5/IoContract';
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
import navlogo from 'assets/images/logos/LOGO-72.png';
import { AnimatePresence, motion } from 'framer-motion';
import PropTypes from 'prop-types';
import useFullscreen from '../../hooks/useFullscreen';
import navigatorConstants from './navigatorConstants';
import style from './NavLogo.module.scss';
/* Styling for action buttons */
const navButtonStyle = {
size: 'md',
variant: 'outline',
colorScheme: 'whiteAlpha',
_hover: { bg: '#ebedf0', color: '#333' },
};
export default function NavLogo(props) {
const { isHidden } = props;
const [showNav, setShowNav] = useState(false);
const { isFullScreen, toggleFullScreen } = useFullscreen();
const handleClick = useCallback(() => {
setShowNav((prev) => !prev);
@@ -45,37 +62,40 @@ export default function NavLogo(props) {
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
whileHover={{ opacity: 1 }}
className={style.navContainer}
data-testid='nav-logo'
>
<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={style.nav}
>
<Link to='/timer' {...tabProps}>Timer</Link>
<Link to='/minimal' {...tabProps}>
Minimal Timer
</Link>
<Link to='/sm' {...tabProps}>
Backstage
</Link>
<Link to='/public' {...tabProps}>
Public
</Link>
<Link to='/lower' {...tabProps}>
Lower Thirds
</Link>
<Link to='/pip' {...tabProps}>
PIP
</Link>
<Link to='/studio' {...tabProps}>
Studio Clock
</Link>
</motion.div>
<>
<motion.div
initial={{ opacity: 0, scaleX: 0 }}
animate={{ opacity: 1, scaleX: 1 }}
whileInView={{ opacity: 1 }}
exit={{ opacity: 0, scaleX: 0 }}
className={style.actions}
>
<IconButton
aria-label='Toggle Fullscreen'
icon={isFullScreen ? <IoContract /> : <IoExpand />}
onClick={toggleFullScreen}
{...navButtonStyle}
/>
</motion.div>
<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={style.nav}
>
{navigatorConstants.map((route) => (
<Link to={route.url} key={route.url} {...tabProps}>
{route.label}
</Link>
))}
</motion.div>
</>
)}
</AnimatePresence>
</motion.div>
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.navContainer {
position: absolute;
@@ -18,6 +18,7 @@
.nav,
.showNav {
margin-top: 2vh;
gap: 2vh;
display: flex;
flex-direction: column;
@@ -29,4 +30,12 @@
color: $text-white;
}
}
.actions {
display: flex;
gap: 8px;
position: absolute;
top: 0.5vw;
right: 75px;
}
}
@@ -0,0 +1,13 @@
const navigatorConstants = [
{ url: '/timer', label: 'Timer' },
{ url: '/clock', label: 'Clock' },
{ url: '/minimal', label: 'Minimal Timer' },
{ url: '/backstage', label: 'Backstage' },
{ url: '/public', label: 'Public' },
{ url: '/lower', label: 'Lower Thirds' },
{ url: '/pip', label: 'PiP' },
{ url: '/studio', label: 'Studio Clock' },
{ url: '/countdown', label: 'Countdown' },
];
export default navigatorConstants;
@@ -1,9 +1,12 @@
import React, { useEffect, useState } from 'react';
import TodayItem from './TodayItem';
import { useInterval } from 'app/hooks/useInterval';
import { useEffect, useState } from 'react';
import { useInterval } from 'common/hooks/useInterval';
import PropTypes from 'prop-types';
import Empty from '../../state/Empty';
import style from './Paginator.module.scss';
import Empty from '../state/Empty';
import TodayItem from './TodayItem';
import './Paginator.scss';
export default function Paginator(props) {
const {
@@ -67,7 +70,7 @@ export default function Paginator(props) {
}
return (
<div className={style.entries}>
<div className='paginator entries'>
{page.map((e) => {
if (e.id === selectedId) selectedState = 1;
else if (selectedState === 1) selectedState = 2;
@@ -0,0 +1,62 @@
@use '../../../theme/viewerDefs' as *;
.paginator.entries {
width: 100%;
.entry-times {
font-family: 'Open Sans', sans-serif;
font-size: 1.4vw;
min-width: 10vw;
opacity: 0.9;
letter-spacing: 0.035em;
}
.entry-title {
align-self: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.entry {
display: flex;
font-size: 1.3vw;
padding: 0.2vh 1vw;
margin-bottom: 1.5vh;
border-radius: 3px;
&--now,
&--future {
color: var(--color-override, $viewer-color);
}
&--past {
font-size: 1.2vw;
opacity: 0.6;
}
&--now {
line-height: 5vh;
background-color: $now-bg-color;
border-bottom: 0.5vh solid;
border-bottom-color: var(--accent-color-override, $accent-color);
margin-left: -0.5em;
padding-left: 0.5em;
}
&--future {
background-color: $future-bg-color;
}
}
.backstage-indicator {
color: var(--accent-color-override, $accent-color);
margin-left: auto;
margin-right: -0.5vw;
&::before {
content: '*';
}
}
}
@@ -0,0 +1,39 @@
import PropTypes from 'prop-types';
import { formatTime } from '../../utils/time';
import './Paginator.scss';
export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
// Format timers
const start = formatTime(timeStart, { format: 'hh:mm' });
const end = formatTime(timeEnd, { format: 'hh:mm' });
// user colours
const userColour = colour !== '' ? colour : 'transparent';
// select styling
let selectStyle = 'entry--past';
if (selected === 1) selectStyle = 'entry--now';
else if (selected === 2) selectStyle = 'entry--future';
return (
<div className={`entry ${selectStyle}`} style={{ borderLeft: `4px solid ${userColour}` }}>
<div className='entry-times'>
{`${start} · ${end}`}
</div>
<div className='entry-title'>{title}</div>
{backstageEvent && <div className='backstage-indicator' />}
</div>
);
}
TodayItem.propTypes = {
selected: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
title: PropTypes.string,
backstageEvent: PropTypes.bool,
colour: PropTypes.string,
};
@@ -1,9 +1,11 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { IconButton } from '@chakra-ui/button';
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { AppContext } from '../../../app/context/AppContext';
import PropTypes from 'prop-types';
import { AppContext } from '../../context/AppContext';
import style from './ProtectRoute.module.scss';
export default function ProtectRoute({ children }) {
@@ -1,5 +1,5 @@
@use '../../../styles/main' as *;
@use '../../../styles/mixins' as *;
@use '../../../theme/main' as *;
@use '../../../theme/mixins' as *;
.container {
background: $bg-black;
@@ -1,12 +1,12 @@
import React from 'react';
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
import PropTypes from 'prop-types';
import style from './Empty.module.scss';
export default function Empty(props) {
const { text, ...rest } = props;
const { text, dark, ...rest } = props;
return (
<div className={style.emptyContainer} {...rest}>
<div className={`${style.emptyContainer} ${dark ? style.dark : ''}`} {...rest}>
<Emptyimage className={style.empty} />
<span className={style.text}>{text}</span>
</div>
@@ -15,4 +15,5 @@ export default function Empty(props) {
Empty.propTypes = {
text: PropTypes.string,
dark: PropTypes.bool,
}
@@ -0,0 +1,27 @@
@use '../../../theme/main' as *;
.emptyContainer {
width: 100%;
height: 100%;
text-align: center;
color: $bg-black-300;
.empty {
width: 100%;
opacity: 0.3;
}
.text {
font-weight: 600;
font-size: 2em;
}
&.dark {
background: $bg-black;
color: $bg-gray-500;
.empty {
opacity: 1;
}
}
}
@@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './TitleCard.module.scss';
export default function TitleCard(props) {
@@ -1,4 +1,4 @@
@use '../../../styles/main' as *;
@use '../../../theme/main' as *;
.label {
@include card-label;
@@ -0,0 +1,26 @@
import PropTypes from 'prop-types';
import './TitleSide.scss';
export default function TitleSide(props) {
const { type, label, title, subtitle, presenter } = props;
const isNext = type === 'next';
return (
<div className='title-side'>
<div className='label'>{label}</div>
<div className={isNext ? 'title next' : 'title now'}>{title}</div>
<div className={isNext ? 'presenter next' : 'presenter now'}>{presenter}</div>
<div className={isNext ? 'subtitle next' : 'subtitle now'}>{subtitle}</div>
</div>
);
}
TitleSide.propTypes = {
type: PropTypes.oneOf(['now', 'next']),
label: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
presenter: PropTypes.string,
}
@@ -0,0 +1,61 @@
@use '../../../theme/viewerDefs' as *;
.title-side {
.label {
font-size: 1.3vw;
color: var(--accent-color-override, $accent-color);
}
.title,
.subtitle,
.presenter {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title {
color: $title-color;
font-weight: 600;
&.now {
font-size: 2.5vw;
}
&.next {
font-size: 2vw;
}
}
.subtitle {
color: $subtitle-color;
&.now {
font-size: 1.2vw;
margin-top: -1vh;
}
&.next {
font-size: 1.1vw;
margin-top: -0.8vh;
}
}
.presenter {
font-weight: 400;
color: $subtitle-color;
&.now {
font-size: 2vw;
}
&.next {
font-size: 1.5vw;
}
}
.title:after,
.subtitle:after,
.presenter:after {
content: '\200b';
}
}
@@ -0,0 +1,45 @@
@use '../../../theme/main' as *;
.modalBody {
min-height: 40vh;
display: flex;
flex-direction: column;
gap: 16px;
.options {
margin-bottom: 1.5em;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.notes {
font-size: 0.9em;
}
.info {
background-color: $bg-gray;
margin: 1em 0;
padding: 0.5em;
border-radius: 2px;
color: black;
position: relative;
}
.corner {
position: absolute;
right: 4px;
top: 4px;
}
.infoList {
font-size: 0.9em;
padding-left: 8px;
}
.flexColumnLeft {
display: flex;
flex-direction: column;
align-items: flex-start;
}
}
@@ -0,0 +1,137 @@
import { ChangeEvent, useCallback, useContext, useRef, useState } from 'react';
import { Button } from '@chakra-ui/button';
import {
Checkbox,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Progress,
} from '@chakra-ui/react';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import { useQueryClient } from '@tanstack/react-query';
import { EVENTS_TABLE } from '../../api/apiConstants';
import { uploadEvents } from '../../api/ontimeApi';
import { LoggingContext } from '../../context/LoggingContext';
import TooltipActionBtn from '../buttons/TooltipActionBtn';
import { validateFile } from './utils';
import style from './UploadModal.module.scss';
interface UploadModalProps {
onClose: () => void;
isOpen: boolean;
}
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext);
const [errors, setErrors] = useState<string[]>([]);
const [file, setFile] = useState<File | null>(null);
const [progress, setProgress] = useState(0);
const overrideOptionRef = useRef<HTMLInputElement>(null);
const handleFile = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const fileUploaded = event?.target?.files?.[0];
if (!fileUploaded) return;
const validate = validateFile(fileUploaded);
setErrors(validate.errors);
if (validate.isValid) {
setFile(fileUploaded);
} else {
setFile(null);
}
}, []);
const handleUpload = useCallback(async () => {
if (file) {
try {
await uploadEvents(file, setProgress, { onlyEvents: overrideOptionRef?.current?.checked });
} catch (error) {
emitError(`Failed uploading file: ${error}`);
} finally {
await queryClient.invalidateQueries(EVENTS_TABLE);
setFile(null);
}
}
}, [emitError, file, queryClient]);
return (
<Modal
onClose={onClose}
isOpen={isOpen}
closeOnOverlayClick={false}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>File upload</ModalHeader>
<ModalCloseButton />
<ModalBody className={style.modalBody}>
<FormControl isInvalid={errors.length > 0}>
<FormLabel>Select file to upload</FormLabel>
<Input type='file' onChange={handleFile} accept='.json, .xlsx' />
{errors.length === 0 ? (
<FormHelperText>.XLSX .JSON with max 1MB</FormHelperText>
) : (
<FormErrorMessage className={style.flexColumnLeft}>
{errors.map((error) => (
<span key={error}>{error}</span>
))}
</FormErrorMessage>
)}
</FormControl>
<div className={style.options}>
<b>Options</b>
<Checkbox ref={overrideOptionRef}>Import only events</Checkbox>
<span className={style.notes}>This will prevent overriding user settings</span>
</div>
{file && (
<div className={style.info}>
<span>File ready to upload</span>
<TooltipActionBtn
clickHandler={() => setFile(null)}
tooltip='Cancel'
aria-label='Cancel'
className={style.corner}
size='sm'
variant='ghosted'
icon={<IoCloseSharp />}
/>
<ul className={style.infoList}>
<li>{file.name}</li>
<li>{`${(file.size / 1024).toFixed(2)}kb`}</li>
<li>{file.type}</li>
</ul>
</div>
)}
<Progress value={progress} />
</ModalBody>
<ModalFooter>
<Button
colorScheme='blue'
disabled={!file || errors.length > 0}
onClick={handleUpload}
isLoading={progress < 0 && progress >= 100}
>
Upload
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,25 @@
type ValidationStatus = {
errors: string[];
isValid: boolean;
};
export function validateFile(file: File): ValidationStatus {
const status:ValidationStatus = { errors: [], isValid: true };
if (!file) {
status.errors.push('No file to upload');
status.isValid = false;
}
// Limit file size to 1MB
if (file.size > 1000000) {
status.errors.push('File size limit (1MB) exceeded');
status.isValid = false;
}
// Check file extension
if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.json')) {
status.errors.push('Unhandled file type');
status.isValid = false;
}
return status;
}
@@ -1,64 +0,0 @@
@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,28 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './TitleSide.module.scss';
export default function TitleSide(props) {
const { type, label, title, subtitle, presenter } = props;
const titleStyle = type === 'next' ? style.nextTitle : style.nowTitle;
const subStyle = type === 'next' ? style.nextSubtitle : style.nowSubtitle;
const presStyle = type === 'next' ? style.nextPresenter : style.nowPresenter;
return (
<>
<div className={style.label}>{label}</div>
<div className={titleStyle}>{title}</div>
<div className={presStyle}>{presenter}</div>
<div className={subStyle}>{subtitle}</div>
</>
);
}
TitleSide.propTypes = {
type: PropTypes.oneOf(['now', 'next']),
label: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
presenter: PropTypes.string,
}
@@ -1,54 +0,0 @@
@use '../../../styles/main' as *;
.label {
@include card-label;
}
.nowTitle,
.nextTitle {
@include card-title;
}
.nowSubtitle,
.nowPresenter,
.nextSubtitle,
.nextPresenter {
color: $subtitle-gray;
}
.nowTitle {
font-size: 2.5vw;
}
.nowPresenter {
font-size: 2vw;
font-weight: 400;
}
.nowSubtitle {
font-size: 1.2vw;
margin-top: -1vh;
}
.nextTitle {
font-size: 2vw;
}
.nextSubtitle {
margin-top: -0.8vh;
font-size: 1.1vw;
}
.nextPresenter {
font-weight: 400;
font-size: 1.5vw;
}
.nowTitle:after,
.nowSubtitle:after,
.nowPresenter:after,
.nextTitle:after,
.nextSubtitle:after,
.nextPresenter:after {
content: '\200b';
}
@@ -1,38 +0,0 @@
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, 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} 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} />}
</div>
);
}
TodayItem.propTypes = {
selected: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
title: PropTypes.string,
backstageEvent: PropTypes.bool,
colour: PropTypes.string,
};
@@ -1,7 +1,8 @@
import React, { createContext, useCallback, useEffect, useState } from 'react';
import { useFetch } from '../hooks/useFetch';
import { createContext, useCallback, useEffect, useState } from 'react';
import { APP_SETTINGS } from '../api/apiConstants';
import { getSettings } from '../api/ontimeApi';
import { useFetch } from '../hooks/useFetch';
export const AppContext = createContext({
auth: false,
@@ -1,4 +1,5 @@
import React, { createContext, useCallback } from 'react';
import { createContext, useCallback } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
export const CollapseContext = createContext({
@@ -1,4 +1,5 @@
import React, { createContext, useCallback, useMemo, useState } from 'react';
import { createContext, useCallback, useMemo, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
export const CursorContext = createContext({
@@ -1,20 +1,47 @@
import React, { createContext, useCallback, useEffect, useState } from 'react';
import { useSocket } from './socketContext';
import { generateId } from '../../common/utils/generate_id';
import { nowInMillis, stringFromMillis } from '../../common/utils/time';
import { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
export const LoggingContext = createContext({
import { generateId } from '../utils/generate_id';
import { nowInMillis, stringFromMillis } from '../utils/time';
import { useSocket } from './socketContext';
type LOG_LEVEL = 'INFO' | 'WARN' | 'ERROR';
type Log = {
id: string;
origin: string;
time: string;
level: LOG_LEVEL;
text: string;
};
interface LoggingProviderState {
logData: Log[];
emitInfo: (text: string) => void;
emitWarning: (text: string) => void;
emitError: (text: string) => void;
clearLog: () => void;
}
type LoggingProviderProps = {
children: ReactNode
}
const notInitialised = () => {
throw new Error("Not initialised");
};
export const LoggingContext = createContext<LoggingProviderState>({
logData: [],
emitInfo: () => undefined,
emitWarning: () => undefined,
emitError: () => undefined,
clearLog: () => undefined,
emitInfo: notInitialised,
emitWarning: notInitialised,
emitError: notInitialised,
clearLog: notInitialised
});
export const LoggingProvider = ({ children }) => {
export const LoggingProvider = ({ children }: LoggingProviderProps) => {
const MAX_MESSAGES = 100;
const socket = useSocket();
const [logData, setLogData] = useState([]);
const [logData, setLogData] = useState<Log[]>([]);
const origin = 'USER';
// handle incoming messages
@@ -24,7 +51,7 @@ export const LoggingProvider = ({ children }) => {
// Ask for log data
socket.emit('get-logger');
socket.on('logger', (data) => {
socket.on('logger', (data: Log) => {
setLogData((l) => [data, ...l]);
});
@@ -41,9 +68,9 @@ export const LoggingProvider = ({ children }) => {
* @private
*/
const _send = useCallback(
(text, level) => {
(text: string, level: LOG_LEVEL) => {
if (socket != null) {
const m = {
const m: Log = {
id: generateId(),
origin,
time: stringFromMillis(nowInMillis()),
@@ -54,7 +81,7 @@ export const LoggingProvider = ({ children }) => {
socket.emit('logger', m);
}
if (logData.length > MAX_MESSAGES) {
setLogData((l) => l.pop());
setLogData((l) => l.slice(1));
}
},
[logData, socket]
@@ -65,7 +92,7 @@ export const LoggingProvider = ({ children }) => {
* @param text
*/
const emitInfo = useCallback(
(text) => {
(text: string) => {
_send(text, 'INFO');
},
[_send]
@@ -76,7 +103,7 @@ export const LoggingProvider = ({ children }) => {
* @param text
*/
const emitWarning = useCallback(
(text) => {
(text: string) => {
_send(text, 'WARN');
},
[_send]
@@ -87,7 +114,7 @@ export const LoggingProvider = ({ children }) => {
* @param text
*/
const emitError = useCallback(
(text) => {
(text: string) => {
_send(text, 'ERROR');
},
[_send]
@@ -1,4 +1,5 @@
import React, { createContext, useCallback, useState } from 'react';
import { createContext, useCallback, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
export const TableSettingsContext = createContext({
@@ -0,0 +1,44 @@
// @ts-nocheck
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { serverURL } from 'common/api/apiConstants';
import io, { Socket } from 'socket.io-client';
interface SocketProviderState {
socket: Socket | null;
emit: <T>(topic: string, payload?: T) => void;
on: <T>(topic: string, callback: (data: T) => void) => void;
off: (topic: string) => void;
}
type SocketProviderProps = {
children: ReactNode;
};
const SocketContext = createContext<SocketProviderState>({
socket: null,
emit: () => {},
on: () => {},
off: () => {}
});
export const useSocket = () => {
return useContext(SocketContext);
};
function SocketProvider({ children }: SocketProviderProps) {
const [socket, setSocket] = useState(null);
useEffect(() => {
const socketInstance = io(serverURL, { transports: ["websocket"] });
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
};
}, []);
return (
<SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
);
}
export default SocketProvider;
@@ -0,0 +1,12 @@
// @ts-nocheck
export default function useElectronEvent() {
const isElectron = window?.process?.type === 'renderer';
const sendToElectron = (channel: string, args: any) => {
if (isElectron) {
window?.ipcRenderer.send(channel, args);
}
};
return { isElectron, sendToElectron };
}
@@ -1,4 +1,5 @@
import { useQuery } from 'react-query';
import { useQuery } from '@tanstack/react-query';
const refetchIntervalMs = 10000;
/**
@@ -10,7 +11,6 @@ export const useFetch = (namespace, fn) => {
const { data, status, isError, refetch } = useQuery(namespace, fn, {
refetchInterval: refetchIntervalMs,
cacheTime: Infinity,
notifyOnChangeProps: 'tracked',
});
return { data, status, isError, refetch };
+31
View File
@@ -0,0 +1,31 @@
import { useCallback, useEffect, useState } from 'react';
export default function useFullscreen() {
const [isFullScreen, setFullScreen] = useState(document.fullscreenElement);
useEffect(() => {
const handleChange = () => {
setFullScreen(document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleChange, { passive: true });
document.addEventListener('resize', handleChange, { passive: true });
return () => {
document.removeEventListener('fullscreenchange', handleChange, { passive: true });
document.removeEventListener('resize', handleChange, { passive: true });
}; }, []);
const toggleFullScreen = useCallback(() => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
setFullScreen(document.fullscreenElement);
}, []);
return { isFullScreen, toggleFullScreen };
}

Some files were not shown because too many files have changed in this diff Show More