Compare commits

..

55 Commits

Author SHA1 Message Date
Carlos Valente 2215319cd1 fix: issue with timer migration (#223)
* fix: issue with timer migration
2022-10-17 15:20:56 +02:00
Carlos Valente 766ff40baa feat: allow rotating viewer (#220)
* feat: allow viewer mirroring
2022-10-17 11:34:18 +02:00
Carlos Valente 61cbce780d fix: rq optimistic mutations (#219)
Fixes a mistake with the react query migration which prevented optimistic mutations from working
2022-10-16 20:58:05 +02:00
Carlos Valente cf963f6e05 fix: correct override css url (#218) 2022-10-16 19:57:52 +02:00
Carlos Valente 320834beab fix issue with vite migration (#217)
* hotfix: 1.8.2 issues vite migration
* fix: file import options
2022-10-13 15:04:13 +02:00
Carlos Valente 723b1cfa48 Fix: issue with text colour (#214)
* fix: adjust text colour from context
* fix: issue with wrong proptype
2022-10-12 20:13:30 +02:00
Carlos Valente f119fb4529 style: revert input style (#211) 2022-09-28 21:14:54 +02:00
Carlos Valente 562cb769c5 Refactor/vite (#209)
* refactor(vite): add dependencies
* refactor(vite): necessary code migrations
* refactor(vite): convert file to tsx
* refactor(vite): config
* refactor(vite): doctype casing
2022-09-28 21:13:20 +02:00
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
Carlos Valente a73f4595a9 feat/123: enable linux build (#127)
* feat/123: enable linux build
* feat/123: style: handle window width gracefully
* feat/123: update README
2022-05-22 09:53:04 +02:00
Carlos Valente f187d5edaf Fix/122 (#128)
* fix/122: register quit shortcut for macOS
* fix/122: version bump
2022-05-21 22:07:43 +02:00
Carlos Valente 17573b9b73 Fix/121 (#124)
* fix/121: database is in public directory
* fix/121: refact loading sequence
* fix/121: populate with demo db
* fix/121: ensure path exists (linux issues)
2022-05-21 13:28:34 +02:00
Carlos Valente 6ac963684d V1 (#118)
* chore: upgrade minor versions
* chore: upgrade cypress
* chore: upgrade fe dependencies
* update readme
* update osx images
* update readme
* upgrade dependencies
* upgrade test dependency
* feat: add option to input autofill
* feat: autofill to the left
* chore: update docs
* chore: cleanup ci
* version bump
* style: prevent zero height bar
* fix: prevent loosing menu
* ux: improve input, no spellcheck or autocomplete
* small ux improvements in cuesheets
* feat 111/increase maximum number of events
* fix: optimistic delete issue with filter
* refact: pincode workflow
* chore: add versioning to packages
2022-05-19 10:48:56 +02:00
Carlos Valente 45ac44b2e8 Update build.yml 2022-04-17 21:29:35 +02:00
Carlos Valente f68b5f1d74 Update build.yml 2022-04-17 20:44:41 +02:00
Carlos Valente db0eee3b9c Feat/mac (#109)
* chore: upgrade dependencies
* feat/mac: draft pipeline
* feat/mac: use public folder for transient files
* feat/mac: set app fullscreen
* feat/mac: cmd + , toggles menu
* feat/mac: update readme
* refact: easier login process
* refact prevent style issues with safari
* refact reduce css download
* style: cleanup paginator design
* style: studio clock is responsive
* style: fix spacing style issues with safari
2022-04-17 20:30:39 +02:00
Carlos Valente 20d3ddbc18 version bump 2022-04-12 19:49:20 +02:00
361 changed files with 16034 additions and 17338 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.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 285 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

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 }}
+190
View File
@@ -0,0 +1,190 @@
name: Ontime build
on:
push:
# run when a tag is created
tags:
- '*'
workflow_dispatch:
jobs:
build_mac:
runs-on: macOS-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '14.x'
# React
- name: React - Install dependencies
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-mac
working-directory: ./server
# Release
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ./server/dist/ontime-macOS.dmg
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_win:
runs-on: windows-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
# React
- name: React - Install dependencies
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-win
working-directory: ./server
# Release
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ./server/dist/ontime-win64.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_linux:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x'
# React
- name: React - Install dependencies
run: yarn install --frozen-lockfile --network-timeout 300000
working-directory: ./client
- name: React - Build project
run: yarn build
working-directory: ./client
# Node server
- name: Server - Install dependencies
run: yarn install --frozen-lockfile --production --network-timeout 300000
working-directory: ./server/src
# App
- name: Electron - Install dependencies
shell: bash
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
working-directory: ./server
- name: Electron - Build app
run: yarn dist-linux
working-directory: ./server
# Release
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ./server/dist/ontime-linux.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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
+3 -19
View File
@@ -9,6 +9,7 @@ jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: ''
@@ -19,20 +20,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 +34,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
@@ -67,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
+146 -44
View File
@@ -1,33 +1,47 @@
[![ontime_test_CI](https://github.com/cpvalente/ontime/actions/workflows/ontime_cy.yml/badge.svg)](https://github.com/cpvalente/ontime/actions/workflows/ontime_cy.yml) [![Ontime build](https://github.com/cpvalente/ontime/actions/workflows/build.yml/badge.svg)](https://github.com/cpvalente/ontime/actions/workflows/build.yml)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-green.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Documentation in Gitbook](https://badges.aleen42.com/src/gitbook_2.svg)](https://cpvalente.gitbook.io/ontime/)
## Download the latest releases here
Download the latest releases here
- [Windows](https://github.com/cpvalente/ontime/releases/latest/download/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>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/></a>
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/dockerhub.png"/></a>
</div>
# Ontime
Ontime is an application for managing event rundowns and running stage timers.
It allows a center application to be able to distribute event information in the local network. This minimises needs for using Media Server outputs or expensive video distribution while allowing easy integration in workflows including OBS and d3.
It allows a center application to be able to distribute event information in the local network. This
minimises needs for using Media Server outputs or expensive video distribution while allowing easy
integration in workflows including OBS and d3.
![App Window](https://github.com/cpvalente/ontime/blob/master/.github/01_app.png)
![App Window](https://github.com/cpvalente/ontime/blob/master/.github/app.jpg)
![Views](https://github.com/cpvalente/ontime/blob/master/.github/02_screentypes.png)
## Using Ontime
Once installed and running, ontime starts a background server that is the heart of all processes.
The app, is used to add / edit your running order in the event list, and running the timers using the Playback Control function.
The app, is used to add / edit your running order in the event list, and running the timers using
the Playback Control function.
From here, any device in the same network with a browser is able to render the views as described. This is done by reaching the ontime server at the _default port 4001_ eg: `localhost:4001` or `192.168.1.3:4001`
You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden).
From here, any device in the same network with a browser is able to render the views as described.
This is done by reaching the ontime server at the _default port 4001_ eg: `localhost:4001`
or `192.168.1.3:4001`
You can then use the ontime logo in the top right corner to select the desired view (event in the
lower thirds view, where it is hidden).
In case of unattended machines or automations, it is possible to use different URL to recall individual views
and extend with using the URL aliases feature
In case of unattended machines or automations, it is possible to use different URL to recall
individual views and extend with using the URL aliases feature
```
For the presentation views...
-------------------------------------------------------------
IP.ADDRESS:4001 > Web server default to presenter timer view
IP.ADDRESS:4001/presenter > Presenter / Stage timer view
IP.ADDRESS:4001/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
@@ -37,76 +51,164 @@ 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/editor
```
More documentation available [here](https://cpvalente.gitbook.io/ontime/)
## Feature List (in no specific order)
- [x] Distribute Data over network and render in the browser
- [x] Different screen types
- Stage Timer
- Backstage Info
- Public Info
- Picture in Picture
- Studio Clock
- Stage Timer
- Backstage Info
- Public Info
- Picture in Picture
- Studio Clock
- [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] 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
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
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?
- [x] a single event with no data is enough to use the OSC API and get going
- [x] use the order list to create a set of quick timers by setting the beginning and start times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC as always
- [x] a single event with no data is enough to use the OSC API and get going
- [x] use the order list to create a set of quick timers by setting the beginning and start
times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC as
always
## Integrations and Workflow
The app is being currently developed to a wide user base, from broadcast to entertainment and conference halls.
## Rich APIs for workflow integrations
Taking advantage of the integrations in Ontime, we currently use Ontime with:
- `disguise`: trigger ontime from d3's timeline using the **OSC API**, **render views** using d3's webmodule
The app is being currently developed to a wide user base, from broadcast to entertainment and
conference halls.
Taking advantage of the integrations in Ontime, we currently use Ontime with:
- `disguise`: trigger ontime from d3's timeline using the **OSC API**, **render views** using d3's
webmodule
- `OBS`: **render views** using the Browser Module
- `QLab`: trigger ontime using **OSC API**
- `Companion`: trigger ontime and manipulate timer using **OSC API**
- `Companion`: Ontime has a **companion module**. Issue report and feature requests should be done
in
the [repository getontime/ontime](https://github.com/bitfocus/companion-module-getontime-ontime)
### Make your own viewer
Ontime broadcasts its data over websockets. This allows you to build your own viewers by leveranging
basic knowledge of HTML + CSS + Javascript (or any other language that can run in the browser).
See [this repository](https://github.com/cpvalente/ontime-viewer-template) with a small template on
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
There are several features planned in the roadmap.
These will be implemented in a development friendly order unless there is user demand to bump any of them.
- [ ] Linux version
- [ ] Headless version (run server only anywhere, configure from a browser locally)
- [ ] Companion module
- [ ] Lower Third Manager
- [ ] Note only event
- [ ] Reach Schedule: way to speedup timer to meet a deadline
- [ ] vMix integration
### For version 1
Almost reaching a feature set that we can call v1. Before that:
- [ ] Mac OS version
### 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)
- [ ] Improvements in event interface
- [ ] Moderator view
- [ ] New playback mode for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
- [ ] Lower Third Manager
- [ ] Reach Schedule: way to speedup timer to meet a deadline
### Issues
The app is still in pre-release and there are a few issues, mainly concerning style.
This will be receiving attention as we near v1 release
#### Style
- [ ] App needs improvement on handling zoomed interfaces: Please ensure that windows settings have no display zoom (it is 125% by default)
- [ ] Very long titles might cause interface to shift
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,
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 might have small inconsistencies
- 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/)
# License
This project is licensed under the terms of the GNU GPL v3
+2
View File
@@ -0,0 +1,2 @@
node_modules
build
+68 -7
View File
@@ -1,16 +1,25 @@
{
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true,
"node": true
},
"extends": [
"react-app",
"react-app/jest",
"plugin:react/recommended"
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier"
],
"plugins": [
"react",
"testing-library",
"jest"
"simple-import-sort"
],
"rules": {
"jest/no-mocks-import": "warn",
"no-useless-concat": "warn",
"prefer-template": "warn",
"react/jsx-no-bind": [
@@ -41,6 +50,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 +88,56 @@
"customValidators": [],
"skipUndeclared": true
}
]
}
],
"simple-import-sort/imports": "warn",
"simple-import-sort/exports": "warn"
},
"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"
]
}
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<link rel='icon' href='/favicon.ico' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta name='theme-color' content='#121212' />
<meta name='ontime' content='ontime - event timer manager' />
<link rel='apple-touch-icon' href='/logo192.png' />
<link
rel='icon'
type='image/png'
sizes='32x32'
href='/favicon-32x32.png'
/>
<link
rel='icon'
type='image/png'
sizes='16x16'
href='/favicon-16x16.png'
/>
<link rel='manifest' href='/site.webmanifest' />
<link rel='manifest' href='/manifest.json' />
<title>ontime</title>
<style>
body,
html {
background-color: rgba(0, 0, 0, 0) !important;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id='root'></div>
<script type='module' src='/src/index.jsx'></script>
</body>
</html>
-6
View File
@@ -1,6 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
+55 -32
View File
@@ -1,39 +1,41 @@
{
"name": "ontime",
"version": "0.1.0",
"name": "ontime-ui",
"version": "1.9.1",
"private": true,
"dependencies": {
"@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",
"@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",
"color": "^4.2.1",
"framer-motion": "4.1.17",
"ontime-utils": "link: ../server/utils/",
"react": "17.0.2",
"axios": "^0.27.2",
"color": "^4.2.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": "^17.0.1",
"react-dom": "^18.1.0",
"react-fast-compare": "^3.2.0",
"react-qr-code": "2.0.3",
"react-query": "^3.34.12",
"react-router-dom": "^6.2.1",
"react-qr-code": "^2.0.5",
"react-router-dom": "^6.3.0",
"react-table": "^7.7.0",
"socket.io-client": "^4.4.1",
"socket.io-client": "^4.5.1",
"typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "set BROWSER=none&&react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
"eject": "react-scripts eject"
"start": "set BROWSER=none&&vite",
"build": "vite build",
"lint": "eslint .",
"stylelint": "npx stylelint \"**/*.scss\"\n",
"test": "vitest ",
"test:pipeline": "vitest run"
},
"browserslist": {
"production": [
@@ -48,13 +50,34 @@
]
},
"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",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^14.1.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",
"@vitejs/plugin-react": "^2.1.0",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.0.4",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-testing-library": "^5.6.4",
"jsdom": "^20.0.0",
"prettier": "^2.7.1",
"prop-types": "^15.8.1",
"react-scripts": "^5.0.0",
"react-test-renderer": "^17.0.2",
"sass": "^1.49.0"
"react-test-renderer": "^18.1.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",
"vite": "^3.1.1",
"vite-plugin-svgr": "^2.2.1",
"vite-tsconfig-paths": "^3.5.0",
"vitest": "^0.23.2"
}
}
}
-59
View File
@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<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" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<style>
body,
html {
background-color: rgba(0, 0, 0, 0) !important;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+30 -90
View File
@@ -1,52 +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 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'));
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);
const ProtectedEditor = () => (
<ProtectRoute>
<Editor />
</ProtectRoute>
);
const ProtectedTable = () => (
<ProtectRoute>
<TableSettingsProvider>
<Table />
</TableSettingsProvider>
</ProtectRoute>
);
// Load Open Sans typeface
import('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) => {
@@ -74,53 +42,25 @@ 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={<ProtectedEditor />} />
<Route path='/cuesheet' element={<ProtectedTable />} />
<Route path='/cuelist' element={<ProtectedTable />} />
<Route path='/table' element={<ProtectedTable />} />
{/* Send to default if nothing found */}
<Route path='*' element={<STimer />} />
</Routes>
</Suspense>
</ErrorBoundary>
</div>
<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,
},
},
});
});
-18
View File
@@ -1,18 +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';
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`;
-12
View File
@@ -1,12 +0,0 @@
import axios from 'axios';
import { eventURL } from './apiConstants';
export const fetchEvent = async () => {
const res = await axios.get(eventURL);
return res.data;
};
export const postEvent = async (data) => {
const res = await axios.post(eventURL, data);
return res;
};
-36
View File
@@ -1,36 +0,0 @@
import axios from 'axios';
import { eventsURL } from './apiConstants';
export const fetchAllEvents = async () => {
const res = await axios.get(eventsURL);
return res.data;
};
export const requestPost = async (data) => {
await axios.post(eventsURL, data);
};
export const requestPut = async (data) => {
await axios.put(eventsURL, data);
};
export const requestPatch = async (data) => {
await axios.patch(eventsURL, data);
};
export const requestReorder = async (data) => {
await axios.patch(`${eventsURL}/reorder`, data);
};
export const requestApplyDelay = async (eventId) => {
const action = 'applydelay';
return await axios.patch(`${eventsURL}/${action}/${eventId}`);
};
export const requestDelete = async (eventId) => {
await axios.delete(`${eventsURL}/${eventId}`);
};
export const requestDeleteAll = async () => {
await axios.delete(`${eventsURL}/all`);
};
-186
View File
@@ -1,186 +0,0 @@
import axios from 'axios';
import { ontimeURL } from './apiConstants';
export const ontimePlaceholderInfo = {
networkInterfaces: [],
settings: {
version: '',
serverPort: 4001,
},
};
export const ontimePlaceholderSettings = {
pinCode: null,
};
export const eventPlaceholderSettings = {
title: '',
url: '',
publicInfo: '',
backstageInfo: '',
endMessage: '',
};
export const userFieldsPlaceholder = {
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
};
export const oscPlaceholderSettings = {
port: '',
portOut: '',
targetIP: '',
enabled: false,
};
export const httpPlaceholder = {
onLoad: {
url: '',
enabled: false,
},
onStart: {
url: '',
enabled: false,
},
onUpdate: {
url: '',
enabled: false,
},
onPause: {
url: '',
enabled: false,
},
onStop: {
url: '',
enabled: false,
},
onFinish: {
url: '',
enabled: false,
},
};
export const ontimeVars = [
{
name: '$timer',
description: 'Current running timer',
},
{
name: '$title',
description: 'Current title',
},
{
name: '$presenter',
description: 'Current timer',
},
{
name: '$subtitle',
description: 'Current subtitle',
},
{
name: '$next-title',
description: 'Next title',
},
{
name: '$next-presenter',
description: 'Next timer',
},
{
name: '$next-subtitle',
description: 'Next subtitle',
},
];
export const getSettings = async () => {
const res = await axios.get(`${ontimeURL}/settings`);
return res.data;
};
export const postSettings = async (data) => {
await axios.post(`${ontimeURL}/settings`, data);
};
export const getInfo = async () => {
const res = await axios.get(`${ontimeURL}/info`);
return res.data;
};
export const postInfo = async (data) => {
await axios.post(`${ontimeURL}/info`, data);
};
export const getAliases = async () => {
const res = await axios.get(`${ontimeURL}/aliases`);
return res.data;
};
export const postAliases = async (data) => {
await axios.post(`${ontimeURL}/aliases`, data);
};
export const getUserFields = async () => {
const res = await axios.get(`${ontimeURL}/userfields`);
return res.data;
};
export const postUserFields = async (data) => {
await axios.post(`${ontimeURL}/userfields`, data);
};
export const getOSC = async () => {
const res = await axios.get(`${ontimeURL}/osc`);
return res.data;
};
export const postOSC = async (data) => {
await axios.post(`${ontimeURL}/osc`, data);
};
export const downloadEvents = async () => {
await axios({
url: `${ontimeURL}/db`,
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
let 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('"');
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
});
};
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',
},
});
};
export const uploadEventsWithPath = async (filepath) => {
await axios.post(`${ontimeURL}/dbpath`, { path: filepath });
};
-37
View File
@@ -1,37 +0,0 @@
import axios from 'axios';
import { playbackURL } from '../api/apiConstants';
export const getStart = async () => {
const res = await axios.get(playbackURL + '/start');
return res;
};
export const getPause = async () => {
const res = axios.get(playbackURL + '/pause');
return res;
};
export const getRoll = async () => {
const res = axios.get(playbackURL + '/roll');
return res;
};
export const getPrevious = async () => {
const res = axios.get(playbackURL + '/previous');
return res;
};
export const getNext = async () => {
const res = axios.get(playbackURL + '/next');
return res;
};
export const getUnload = async () => {
const res = axios.get(playbackURL + '/unload');
return res;
};
export const getReload = async () => {
const res = axios.get(playbackURL + '/reload');
return res;
};
@@ -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' },
+33
View File
@@ -0,0 +1,33 @@
export const STATIC_PORT = 4001;
export const EVENT_TABLE = ['event'];
export const ALIASES = ['aliases'];
export const USERFIELDS = ['userFields'];
export const EVENTS_TABLE_KEY = 'events';
export const EVENTS_TABLE = [EVENTS_TABLE_KEY];
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 = () =>
import.meta.env.DEV ? `http://localhost:${STATIC_PORT}` : 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}`;
+18
View File
@@ -0,0 +1,18 @@
import axios from 'axios';
import { eventURL } from './apiConstants';
/**
* @description HTTP request to fetch event data
* @return {Promise}
*/
export const fetchEvent = async () => {
const res = await axios.get(eventURL);
return res.data;
};
/**
* @description HTTP request to mutate event data
* @return {Promise}
*/
export const postEvent = async (data) => axios.post(eventURL, data);
+54
View File
@@ -0,0 +1,54 @@
import axios from 'axios';
import { eventsURL } from './apiConstants';
/**
* @description HTTP request to fetch all events
* @return {Promise}
*/
export const fetchAllEvents = async () => {
const res = await axios.get(eventsURL);
return res.data;
};
/**
* @description HTTP request to post new event
* @return {Promise}
*/
export const requestPost = async (data) => axios.post(eventsURL, data);
/**
* @description HTTP request to put new event
* @return {Promise}
*/
export const requestPut = async (data) => axios.put(eventsURL, data);
/**
* @description HTTP request to modify event
* @return {Promise}
*/
export const requestPatch = async (data) => axios.patch(eventsURL, data);
/**
* @description HTTP request to reorder events
* @return {Promise}
*/
export const requestReorder = async (data) => axios.patch(`${eventsURL}/reorder`, data);
/**
* @description HTTP request to request application of delay
* @return {Promise}
*/
export const requestApplyDelay = async (eventId) => axios.patch(`${eventsURL}/applydelay/${eventId}`);
/**
* @description HTTP request to delete given event
* @return {Promise}
*/
export const requestDelete = async (eventId) => axios.delete(`${eventsURL}/${eventId}`);
/**
* @description HTTP request to delete all events
* @return {Promise}
*/
export const requestDeleteAll = async () => axios.delete(`${eventsURL}/all`);
+289
View File
@@ -0,0 +1,289 @@
import axios from 'axios';
import { ontimeURL } from './apiConstants';
/**
* @description placeholder information for ontimeInfo
* @type {{settings: {serverPort: number, version: string}, networkInterfaces: *[]}}
*/
export const ontimePlaceholderInfo = {
networkInterfaces: [],
settings: {
version: '',
serverPort: 4001,
},
};
/**
* @description placeholder information for ontimeSettings
* @type {{pinCode: null}}
*/
export const ontimePlaceholderSettings = {
app: 'ontime',
version: 1,
serverPort: 4001,
lock: null,
pinCode: null,
timeFormat: '24',
};
/**
* @description placeholder information for eventSettings
* @type {{backstageInfo: string, endMessage: string, publicInfo: string, title: string, url: string}}
*/
export const eventPlaceholderSettings = {
title: '',
url: '',
publicInfo: '',
backstageInfo: '',
endMessage: '',
};
/**
* @description placeholder information for userFields
* @type {{user1: string, user2: string, user0: string, user9: string, user7: string, user8: string, user5: string, user6: string, user3: string, user4: string}}
*/
export const userFieldsPlaceholder = {
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
};
/**
* @description placeholder information for oscSettings
* @type {{targetIP: string, port: string, portOut: string, enabled: boolean}}
*/
export const oscPlaceholderSettings = {
port: '',
portOut: '',
targetIP: '',
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}}}
*/
export const httpPlaceholder = {
onLoad: {
url: '',
enabled: false,
},
onStart: {
url: '',
enabled: false,
},
onUpdate: {
url: '',
enabled: false,
},
onPause: {
url: '',
enabled: false,
},
onStop: {
url: '',
enabled: false,
},
onFinish: {
url: '',
enabled: false,
},
};
/**
* @description ontime utility variables
* @type {[{name: string, description: string}, {name: string, description: string}, {name: string, description: string}, {name: string, description: string}, {name: string, description: string}, null, null]}
*/
export const ontimeVars = [
{
name: '$timer',
description: 'Current running timer',
},
{
name: '$title',
description: 'Current title',
},
{
name: '$presenter',
description: 'Current timer',
},
{
name: '$subtitle',
description: 'Current subtitle',
},
{
name: '$next-title',
description: 'Next title',
},
{
name: '$next-presenter',
description: 'Next timer',
},
{
name: '$next-subtitle',
description: 'Next subtitle',
},
];
/**
* @description HTTP request to retrieve application settings
* @return {Promise}
*/
export const getSettings = async () => {
const res = await axios.get(`${ontimeURL}/settings`);
return res.data;
};
/**
* @description HTTP request to mutate application settings
* @return {Promise}
*/
export const postSettings = async (data) => axios.post(`${ontimeURL}/settings`, data);
/**
* @description HTTP request to retrieve application info
* @return {Promise}
*/
export const getInfo = async () => {
const res = await axios.get(`${ontimeURL}/info`);
return res.data;
};
/**
* @description HTTP request to mutate application info
* @return {Promise}
*/
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}
*/
export const getAliases = async () => {
const res = await axios.get(`${ontimeURL}/aliases`);
return res.data;
};
/**
* @description HTTP request to mutate aliases
* @return {Promise}
*/
export const postAliases = async (data) => axios.post(`${ontimeURL}/aliases`, data);
/**
* @description HTTP request to retrieve user fields
* @return {Promise}
*/
export const getUserFields = async () => {
const res = await axios.get(`${ontimeURL}/userfields`);
return res.data;
};
/**
* @description HTTP request to mutate user fields
* @return {Promise}
*/
export const postUserFields = async (data) => axios.post(`${ontimeURL}/userfields`, data);
/**
* @description HTTP request to retrieve osc settings
* @return {Promise}
*/
export const getOSC = async () => {
const res = await axios.get(`${ontimeURL}/osc`);
return res.data;
};
/**
* @description HTTP request to mutate osc settings
* @return {Promise}
*/
export const postOSC = async (data) => axios.post(`${ontimeURL}/osc`, data);
/**
* @description HTTP request to download db
* @return {Promise}
*/
export const downloadEvents = async () => {
await axios({
url: `${ontimeURL}/db`,
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
const headerLine = response.headers['Content-Disposition'];
let filename = 'events.json';
// try and get the filename from the response
if (headerLine != null) {
const startFileNameIndex = headerLine.indexOf('"') + 1;
const endFileNameIndex = headerLine.lastIndexOf('"');
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
});
};
/**
* @description HTTP request to upload events db
* @return {Promise}
*/
export const uploadEvents = async (file, setProgress, options) => {
const formData = new FormData();
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 });
+45
View File
@@ -0,0 +1,45 @@
import axios from 'axios';
import { playbackURL } from './apiConstants';
/**
* @description HTTP call to start current timer
* @return {Promise}
*/
export const getStart = async () => axios.get(`${playbackURL}/start`);
/**
* @description HTTP call to pause current timer
* @return {Promise}
*/
export const getPause = async () => axios.get(`${playbackURL}/pause`);
/**
* @description HTTP call to start roll mode
* @return {Promise}
*/
export const getRoll = async () => axios.get(`${playbackURL}/roll`);
/**
* @description HTTP call to skip to previous event
* @return {Promise}
*/
export const getPrevious = async () => axios.get(`${playbackURL}/previous`);
/**
* @description HTTP call to skip to next event
* @return {Promise}
*/
export const getNext = async () => axios.get(`${playbackURL}/next`);
/**
* @description HTTP call to unload current timer
* @return {Promise}
*/
export const getUnload = async () => axios.get(`${playbackURL}/unload`);
/**
* @description HTTP call to reload current timer
* @return {Promise}
*/
export const getReload = async () => axios.get(`${playbackURL}/reload`);
@@ -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
);
@@ -0,0 +1,3 @@
import { atomWithStorage } from 'jotai/utils';
export const mirrorViewersAtom = atomWithStorage('ontime-viewers-mirrorViewers', false);
@@ -1,10 +1,12 @@
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;
@@ -16,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'
@@ -47,3 +49,10 @@ export default function ActionButtons(props) {
</Menu>
);
}
ActionButtons.propTypes = {
showAdd: PropTypes.bool,
showDelay: PropTypes.bool,
showBlock: PropTypes.bool,
actionHandler: PropTypes.func,
}
@@ -1,18 +0,0 @@
import { IconButton } from '@chakra-ui/button';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
export default function AddIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<IconButton
size={props.size || 'xs'}
icon={<FiPlus />}
_expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }}
backgroundColor={'orange.200'}
color={'orange.500'}
onClick={clickhandler}
{...rest}
/>
);
}
@@ -1,20 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
export default function ApplyIconBtn(props) {
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Apply delays'>
<IconButton
size={size || 'xs'}
icon={<FiCheck />}
colorScheme='orange'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,16 +0,0 @@
import { IconButton } from '@chakra-ui/button';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
export default function BlockIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<IconButton
size={props.size || 'xs'}
icon={<FiMinusCircle />}
colorScheme='purple'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
);
}
@@ -1,21 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiChevronsUp } from '@react-icons/all-files/fi/FiChevronsUp';
export default function CollapseBtn(props) {
const { clickhandler, size } = props;
return (
<Tooltip label='Collapse all'>
<IconButton
size={size || 'xs'}
icon={<FiChevronsUp />}
colorScheme='white'
variant='outline'
background='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
/>
</Tooltip>
);
}
@@ -1,18 +0,0 @@
import { Button } from '@chakra-ui/button';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
export default function CurrentBtn(props) {
const { clickhandler, active } = props;
return (
<Button
size={props.size || 'xs'}
leftIcon={<FiTarget />}
colorScheme='whiteAlpha'
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
>
Goto Current
</Button>
);
}
@@ -1,23 +0,0 @@
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>
);
}
@@ -1,23 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
export default function CursorLockedBtn(props) {
const { clickhandler, active, ref, size } = props;
return (
<Tooltip label='Lock cursor to current'>
<IconButton
ref={ref}
size={size || 'xs'}
icon={<FiTarget />}
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,23 +0,0 @@
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,16 +0,0 @@
import { IconButton } from '@chakra-ui/button';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
export default function DelayIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<IconButton
size={props.size || 'xs'}
icon={<FiClock />}
colorScheme='yellow'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
);
}
@@ -1,30 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { useState } from 'react';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { Tooltip } from '@chakra-ui/tooltip';
export default function DeleteIconBtn(props) {
const { actionHandler, size, ...rest } = props;
const [loading, setLoading] = useState(false);
const handleClick = () => {
setLoading(true);
actionHandler('delete');
};
return (
<Tooltip label='Delete'>
<IconButton
size={size || 'xs'}
icon={<IoRemove />}
colorScheme='red'
onClick={handleClick}
_focus={{ boxShadow: 'none' }}
disabled={loading}
isLoading={loading}
{...rest}
/>
</Tooltip>
);
}
@@ -1,20 +1,26 @@
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) {
const { active, text, actionHandler, size } = props;
const { active, text, actionHandler, size = 'xs' } = props;
return (
<Button
size={size || 'xs'}
size={size}
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={actionHandler}
_focus={{ boxShadow: 'none' }}
>
{text}
</Button>
);
}
EnableBtn.propTypes = {
active: PropTypes.bool,
text: PropTypes.string,
actionHandler: PropTypes.func,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
}
@@ -1,21 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiChevronsDown } from '@react-icons/all-files/fi/FiChevronsDown';
export default function ExpandBtn(props) {
const { clickhandler, size } = props;
return (
<Tooltip label='Expand all'>
<IconButton
size={size || 'xs'}
icon={<FiChevronsDown />}
colorScheme='white'
variant='outline'
background='#fff1'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
/>
</Tooltip>
);
}
@@ -1,22 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { Tooltip } from '@chakra-ui/tooltip';
export default function NextIconBtn(props) {
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlaySkipForward size='22px' />}
colorScheme='whiteAlpha'
backgroundColor='#ffffff11'
variant='outline'
onClick={clickhandler}
width={90}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,22 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
import { Tooltip } from '@chakra-ui/tooltip';
export default function OnAirIconBtn(props) {
const { actionHandler, active, size, ...rest } = props;
return (
<Tooltip label={active ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
<IconButton
size={size || 'xs'}
icon={active ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,21 +1,30 @@
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'
_hover={!disabled && { bg: 'orange.400' }}
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
width={120}
_focus={{ boxShadow: 'none' }}
disabled={disabled}
{...rest}
/>
</Tooltip>
);
}
PauseIconBtn.propTypes = {
clickhandler: PropTypes.func,
active: PropTypes.bool,
disabled: PropTypes.bool
}
@@ -1,22 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
import { Tooltip } from '@chakra-ui/tooltip';
export default function PrevIconBtn(props) {
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlaySkipBack size='22px' />}
colorScheme='whiteAlpha'
backgroundColor='#ffffff11'
variant='outline'
onClick={clickhandler}
width={90}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,21 +1,26 @@
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';
import PropTypes from 'prop-types';
export default function PublicIconBtn(props) {
const { actionHandler, active, size, ...rest } = props;
const { actionHandler, active, size = 'xs', ...rest } = props;
return (
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
<IconButton
size={size || 'xs'}
size={size}
icon={<FiUsers />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
PublicIconBtn.propTypes = {
actionHandler: PropTypes.func,
active: PropTypes.bool,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
};
@@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Button, IconButton } from '@chakra-ui/button';
import {
AlertDialog,
@@ -10,23 +10,36 @@ import {
} from '@chakra-ui/modal';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiPower } from '@react-icons/all-files/fi/FiPower';
import PropTypes from 'prop-types';
import { LoggingContext } from '../../context/LoggingContext';
export default function QuitIconBtn(props) {
const { clickhandler, size, ...rest } = props;
const { clickHandler, size = 'lg', ...rest } = props;
const [isOpen, setIsOpen] = useState(false);
const { emitInfo } = useContext(LoggingContext);
const onClose = () => setIsOpen(false);
const cancelRef = useRef();
const handleShutdown = () => {
useEffect(() => {
if (window.process?.type === 'renderer') {
window.ipcRenderer.on('user-request-shutdown', () => {
emitInfo('Shutdown request')
setIsOpen(true);
});
}
}, [emitInfo]);
const handleShutdown = useCallback(() => {
onClose();
clickhandler();
};
clickHandler();
}, [clickHandler]);
return (
<>
<Tooltip label='Quit Application'>
<IconButton
size={size || 'xs'}
size={size}
icon={<FiPower />}
colorScheme='red'
variant='outline'
@@ -36,22 +49,15 @@ export default function QuitIconBtn(props) {
{...rest}
/>
</Tooltip>
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
>
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
Server Shutdown
</AlertDialogHeader>
<AlertDialogBody>
This will shutdown the program and all running servers. Are you
sure?
This will shutdown the program and all running servers. Are you sure?
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
Cancel
@@ -66,3 +72,8 @@ export default function QuitIconBtn(props) {
</>
);
}
QuitIconBtn.propTypes = {
clickHandler: PropTypes.func,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
};
@@ -1,22 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoArrowUndo } from '@react-icons/all-files/io5/IoArrowUndo';
import { Tooltip } from '@chakra-ui/tooltip';
export default function ReloadIconButton(props) {
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoArrowUndo size='22px' />}
colorScheme='whiteAlpha'
backgroundColor='#ffffff05'
variant='outline'
onClick={clickhandler}
width={90}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,21 +1,29 @@
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'
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
width={120}
_focus={{ boxShadow: 'none' }}
disabled={disabled}
{...rest}
/>
</Tooltip>
);
}
RollIconBtn.propTypes = {
clickhandler: PropTypes.func,
active: PropTypes.bool,
disabled: PropTypes.bool
}
@@ -1,21 +1,29 @@
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'
variant={active ? 'solid' : 'outline'}
onClick={clickhandler}
width={120}
_focus={{ boxShadow: 'none' }}
disabled={disabled}
{...rest}
/>
</Tooltip>
);
}
StartIconBtn.propTypes = {
clickhandler: PropTypes.func,
active: PropTypes.bool,
disabled: PropTypes.bool
}
@@ -0,0 +1,25 @@
import { IconButton } from '@chakra-ui/button';
import { IconButtonProps } from '@chakra-ui/react';
import { Tooltip } from '@chakra-ui/tooltip';
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>
);
}
@@ -0,0 +1,36 @@
import { useCallback, useState } from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import PropTypes from 'prop-types';
export default function TooltipLoadingActionBtn(props) {
const { clickHandler, icon, color, size = 'xs', tooltip, ...rest } = props;
const [loading, setLoading] = useState(false);
const handleClick = useCallback(() => {
setLoading(true);
clickHandler();
},[clickHandler, setLoading]);
return (
<Tooltip label={tooltip} shouldWrapChildren={loading}>
<IconButton
aria-label={tooltip}
size={size}
icon={icon}
onClick={handleClick}
disabled={loading}
isLoading={loading}
{...rest}
/>
</Tooltip>
);
}
TooltipLoadingActionBtn.propTypes = {
clickHandler: PropTypes.func,
icon: PropTypes.element,
color: PropTypes.string,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
tooltip: PropTypes.string,
};
@@ -0,0 +1,30 @@
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={tooltipDelayMid} shouldWrapChildren={disabled}>
<IconButton
icon={icon}
colorScheme='white'
variant='outline'
_hover={!disabled && { bg: '#ebedf0', color: '#333' }}
onClick={clickHandler}
width={90}
disabled={disabled}
{...rest}
/>
</Tooltip>
);
}
TransportIconBtn.propTypes = {
clickHandler: PropTypes.func,
icon: PropTypes.element,
tooltip: PropTypes.string,
disabled: PropTypes.bool,
};
@@ -1,16 +0,0 @@
import { IconButton } from '@chakra-ui/button';
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
export default function TrashIconBtn(props) {
const { clickhandler, ...rest } = props;
return (
<IconButton
size={props.size || 'xs'}
icon={<FiTrash2 />}
colorScheme='red'
onClick={clickhandler}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
);
}
@@ -1,21 +1,28 @@
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;
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'
variant='outline'
onClick={clickhandler}
onClick={clickHandler}
width={90}
_focus={{ boxShadow: 'none' }}
disabled={disabled}
{...rest}
/>
</Tooltip>
);
}
UnloadIconBtn.propTypes = {
clickHandler: PropTypes.func,
disabled: PropTypes.bool,
};
@@ -1,23 +0,0 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import { Tooltip } from '@chakra-ui/tooltip';
export default function VisibleIconBtn(props) {
const { actionHandler, active, size, ...rest } = props;
return (
<Tooltip label={active ? 'Make invisible' : 'Make visible'} openDelay={500}>
<IconButton
size={size || 'xs'}
icon={<IoSunny size='18px' />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={() =>
actionHandler('update', { field: 'isPublic', value: !active })
}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
</Tooltip>
);
}
@@ -1,14 +1,14 @@
import React from 'react';
import PropTypes from "prop-types";
import style from "../../../features/info/Info.module.scss";
import {Icon} from "@chakra-ui/react";
import {FiChevronUp} from "@react-icons/all-files/fi/FiChevronUp";
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) {
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}
@@ -16,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 '../../../theme/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,32 +0,0 @@
import React, { memo } from 'react';
import { formatDisplay } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import styles from './Countdown.module.css';
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
// prepare display string
const display =
time != null && !isNaN(time)
? formatDisplay(time, hideZeroHours)
: '-- : -- : --';
const colour = isNegative ? '#ff7597' : '#fffffa';
return (
<div
className={small ? styles.countdownClockSmall : styles.countdownClock}
style={{ color: colour }}
>
{display}
</div>
);
};
export default memo(Countdown);
Countdown.propTypes = {
time: PropTypes.number,
small: PropTypes.bool,
isNegative: PropTypes.bool,
hideZeroHours: PropTypes.bool,
};
@@ -1,25 +0,0 @@
/* @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap'); */
/* Common */
.countdownClock,
.countdownClockSmall {
font-family: 'Open Sans', sans-serif;
line-height: 0.9;
}
/* 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;
color: #fffffa;
}
@@ -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,9 +1,14 @@
/* eslint-disable react/destructuring-assignment */
import React from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { version as appVersion } from '../../../../package.json';
import { LoggingContext } from '../../context/LoggingContext';
import style from './ErrorBoundary.module.scss';
class ErrorBoundary extends React.Component {
static contextType = LoggingContext;
reportContent = '';
constructor(props) {
super(props);
@@ -20,12 +25,47 @@ class ErrorBoundary extends React.Component {
error: error,
errorInfo: info,
});
this.context.emitError(error.toString());
try {
this.context.emitError(error.toString());
} catch (e) {
console.log('Unable to emit error', error, e )
}
this.reportContent = `${error} ${info.componentStack}`;
}
render() {
if (this.state.errorMessage) {
return <p>:/</p>;
return (
<div className={style.errorContainer} data-testid="error-container">
<div>
<p className={style.error}>:/</p>
<p>Something went wrong</p>
<p
className={style.report}
onClick={() => {
if (navigator.clipboard) {
const copyContent = `ontime version ${appVersion} \n ${this.reportContent}`;
navigator.clipboard.writeText(copyContent);
}
}}
>
Copy error
</p>
<p
className={style.report}
onClick={() => {
if (window.process.type === 'renderer') {
window.ipcRenderer.send('reload');
} else {
window.location.reload();
}
}}
>
Reload interface
</p>
</div>
</div>
);
}
return this.props.children;
}
@@ -0,0 +1,28 @@
@use '../../../theme/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,9 +1,10 @@
import React, { 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);
@@ -15,29 +16,28 @@ export default function EventTimes(props) {
* @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;
const handleValidate = useCallback(
() => (entry, val) => {
if (val == null || timeStart == null || timeEnd == null) return true;
if (timeStart === 0) return true;
let start = timeStart;
let end = timeEnd;
if (entry === 'timeStart') {
start = val;
} else if (entry === 'timeEnd') {
end = val;
} else if (entry === 'durationOverride'){
return true;
} else {
return false
}
let start = timeStart;
let end = timeEnd;
if (entry === 'timeStart') {
start = val;
} else if (entry === 'timeEnd') {
end = val;
} else return entry === 'durationOverride';
const valid = validateTimes(start, end);
// give warning but not enforce validation
if (!valid.value) {
emitWarning(`Time Input Warning: ${valid.catch}`);
}
return valid.value;
};
const valid = validateTimes(start, end);
// give warning but not enforce validation
if (!valid.value) {
emitWarning(`Time Input Warning: ${valid.catch}`);
}
return valid.value;
},
[emitWarning, timeEnd, timeStart]
);
return (
<>
@@ -1,112 +1,11 @@
import React, { useContext } from 'react';
import EditableTimer from 'common/input/EditableTimer';
import { stringFromMillis } from 'ontime-utils/time';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import { useCallback, useContext } from 'react';
import PropTypes from 'prop-types';
const label = {
fontSize: '0.75em',
color: '#aaa',
};
import { LoggingContext } from '../../context/LoggingContext';
import { validateTimes } from '../../utils/entryValidator';
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,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
duration: PropTypes.number,
previousEnd: PropTypes.number,
};
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,
timeEnd: PropTypes.number,
duration: PropTypes.number,
previousEnd: PropTypes.number,
};
import Times from './Times';
import TimesDelayed from './TimesDelayed';
export default function EventTimesVertical(props) {
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
@@ -119,29 +18,28 @@ export default function EventTimesVertical(props) {
* @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;
const handleValidate = useCallback(
() => (entry, val) => {
if (val == null || timeStart == null || timeEnd == null) return true;
if (timeStart === 0) return true;
let start = timeStart;
let end = timeEnd;
if (entry === 'timeStart') {
start = val;
} else if (entry === 'timeEnd') {
end = val;
} else if (entry === 'durationOverride') {
return true;
} else {
return false;
}
let start = timeStart;
let end = timeEnd;
if (entry === 'timeStart') {
start = val;
} else if (entry === 'timeEnd') {
end = val;
} else return entry === 'durationOverride';
const valid = validateTimes(start, end);
// give warning but not enforce validation
if (!valid.value) {
emitWarning(`Time Input Warning: ${valid.catch}`);
}
return valid.value;
};
const valid = validateTimes(start, end);
// give warning but not enforce validation
if (!valid.value) {
emitWarning(`Time Input Warning: ${valid.catch}`);
}
return valid.value;
},
[emitWarning, timeEnd, timeStart]
);
return delay != null && delay !== 0 ? (
<TimesDelayed
@@ -0,0 +1,50 @@
import PropTypes from 'prop-types';
import EditableTimer from '../input/EditableTimer';
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 '../../../theme/main' as *;
.label {
font-size: 0.75em;
color: $label-gray;
}
@@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import { stringFromMillis } from '../../utils/time';
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;
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,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, { 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,
@@ -24,14 +26,17 @@ export default function DelayInput(props) {
setValue(value);
}, [value]);
const handleSubmit = (val) => {
if (val === value) return;
if (val === '') setValue(0);
const handleSubmit = useCallback(
(newValue) => {
if (newValue === value) return;
if (newValue === '') setValue(0);
// convert to ms and updates
const msVal = clamp(val, -60, 60) * 60000;
actionHandler('update', { field: 'duration', value: msVal });
};
// convert to ms and updates
const msVal = clamp(newValue, -60, 60) * 60000;
actionHandler('update', { field: 'duration', value: msVal });
},
[actionHandler, value]
);
const labelText = `minutes ${value >= 0 ? 'delayed' : 'ahead'}`;
@@ -1,8 +1,9 @@
import React, { 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 || '');
@@ -12,7 +13,7 @@ export default function EditableText(props) {
else setText(defaultValue);
}, [defaultValue]);
const handleSubmit = (submittedVal) => {
const handleSubmit = useCallback((submittedVal) => {
// No need to update if it hasnt changed
if (submittedVal === defaultValue) return;
// submit a cleaned up version of the string
@@ -22,11 +23,11 @@ export default function EditableText(props) {
if (cleanVal !== submittedVal) {
setText(cleanVal);
}
};
},[defaultValue, submitHandler]);
const handleChange = (val) => {
const handleChange = useCallback((val) => {
if (val.length < maxchar) setText(val);
};
},[maxchar]);
return (
<div className={style.block}>
@@ -1,4 +1,4 @@
@use '../../styles/main' as *;
@use '../../../theme/main' as *;
.block {
overflow: hidden;
@@ -15,7 +15,7 @@
}
.preview {
color: #666;
color: $bg-gray-500;
}
.inline {
@@ -1,37 +1,20 @@
import React, { 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 { stringFromMillis } from 'ontime-utils/time';
import style from './EditableTimer.module.css';
import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types';
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) {
const { name, actionHandler, time, delay, validate, previousEnd } = props;
const { name, actionHandler, time = 0, delay, validate, previousEnd } = props;
const { emitError } = useContext(LoggingContext);
const [value, setValue] = useState('');
// prepare time fields
const validateValue = (value) => {
const success = handleSubmit(value);
if (success) {
const ms = forgivingStringToMillis(value);
setValue(stringFromMillis(ms + delay));
} else {
setValue(stringFromMillis(time + delay));
}
};
useEffect(() => {
if (time == null) return;
try {
setValue(stringFromMillis(time + delay));
} catch (error) {
emitError(`Unable to parse date: ${error.text}`);
}
}, [time, delay, emitError]);
const handleSubmit = (value) => {
const handleSubmit = useCallback((value) => {
// Check if there is anything there
if (value === '') return false;
@@ -65,7 +48,27 @@ export default function EditableTimer(props) {
actionHandler('update', { field: name, value: newValMillis });
return true;
};
},[actionHandler, delay, name, previousEnd, time, validate]);
// prepare time fields
const validateValue = useCallback((value) => {
const success = handleSubmit(value);
if (success) {
const ms = forgivingStringToMillis(value);
setValue(stringFromMillis(ms + delay));
} else {
setValue(stringFromMillis(time + delay));
}
},[delay, handleSubmit, time]);
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;
@@ -1,7 +1,9 @@
@use '../../../theme/main'as *;
.editable,
.delayedEditable {
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
background-color: $input-bg;
border: $input-border;
width: 6.5em;
letter-spacing: 1px;
height: fit-content;
@@ -11,5 +13,5 @@
}
.delayedEditable {
border: 1px solid #d69e2e55;
border: $input-delayed-border;
}
@@ -1,6 +1,7 @@
import React from 'react';
import { clamp } from 'app/utils/math';
import styles from './MyProgressBar.module.css';
import { clamp } from 'common/utils/math';
import PropTypes from 'prop-types';
import styles from './MyProgressBar.module.scss';
export default function MyProgressBar(props) {
const { now, complete, showElapsed } = props;
@@ -22,3 +23,9 @@ export default function MyProgressBar(props) {
</div>
);
}
MyProgressBar.propTypes = {
now: PropTypes.number,
complete: PropTypes.number,
showElapsed: PropTypes.bool,
}
@@ -1,22 +1,23 @@
@use '../../../theme/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;
+75 -72
View File
@@ -1,18 +1,39 @@
import React, { useState, useEffect, useCallback } 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 { IoSync } from '@react-icons/all-files/io5/IoSync';
import navlogo from 'assets/images/logos/LOGO-72.png';
import { AnimatePresence, motion } from 'framer-motion';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types';
import { mirrorViewersAtom } from '../../atoms/ViewerSettings';
import useFullscreen from '../../hooks/useFullscreen';
import navigatorConstants from './navigatorConstants';
import style from './NavLogo.module.scss';
export default function NavLogo(props) {
const {isHidden} = props;
const [showNav, setShowNav] = useState(false);
/* Styling for action buttons */
const navButtonStyle = {
size: 'md',
variant: 'outline',
colorScheme: 'whiteAlpha',
_hover: { bg: '#ebedf0', color: '#333' },
};
const handleClick = () => {
setShowNav(!showNav);
};
export default function NavLogo(props) {
const { isHidden } = props;
const [showNav, setShowNav] = useState(false);
const { isFullScreen, toggleFullScreen } = useFullscreen();
const [isMirrored, setMirrored] = useAtom(mirrorViewersAtom);
const handleClick = useCallback(() => {
setShowNav((prev) => !prev);
}, []);
// Handle keyboard shortcuts
const handleKeyPress = useCallback((e) => {
@@ -34,77 +55,59 @@ export default function NavLogo(props) {
};
}, [handleKeyPress]);
const baseOpacity = (isHidden) ? 0 : 0.5
const baseOpacity = isHidden ? 0 : 0.5;
const tabProps = {
className: style.navItem,
tabIndex: 0,
};
console.log('debug', isMirrored)
return (
<motion.div
initial={{ opacity: baseOpacity }}
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}
/>
<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 }}
exit={{ opacity: 0, scaleY: 0, y: -50 }}
className={showNav ? style.nav : style.navHidden}
>
<Link
to='/timer'
className={style.navItem}
tabIndex={1}
<>
<motion.div
initial={{ opacity: 0, scaleX: 0 }}
animate={{ opacity: 1, scaleX: 1 }}
whileInView={{ opacity: 1 }}
exit={{ opacity: 0, scaleX: 0 }}
className={style.actions}
>
Timer
</Link>
<Link
to='/minimal'
className={style.navItem}
tabIndex={2}
<IconButton
aria-label='Mirror screen'
icon={<IoSync />}
onClick={() => setMirrored((prev) => !prev)}
{...navButtonStyle}
/>
<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}
>
Minimal Timer
</Link>
<Link
to='/sm'
className={style.navItem}
tabIndex={3}
>
Backstage
</Link>
<Link
to='/public'
className={style.navItem}
tabIndex={4}
>
Public
</Link>
<Link
to='/lower'
className={style.navItem}
tabIndex={5}
>
Lower Thirds
</Link>
<Link
to='/pip'
className={style.navItem}
tabIndex={6}
>
PIP
</Link>
<Link
to='/studio'
className={style.navItem}
tabIndex={7}
>
Studio Clock
</Link>
</motion.div>
{navigatorConstants.map((route) => (
<Link to={route.url} key={route.url} {...tabProps}>
{route.label}
</Link>
))}
</motion.div>
</>
)}
</AnimatePresence>
</motion.div>
@@ -113,4 +116,4 @@ export default function NavLogo(props) {
NavLogo.propTypes = {
isHidden: PropTypes.bool,
}
};
@@ -1,4 +1,4 @@
$nav-color: #fff;
@use '../../../theme/main' as *;
.navContainer {
position: absolute;
@@ -9,22 +9,33 @@ $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 {
margin-top: 2vh;
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;
}
}
.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;
@@ -0,0 +1,101 @@
import { useEffect, useState } from 'react';
import { useInterval } from 'common/hooks/useInterval';
import PropTypes from 'prop-types';
import Empty from '../state/Empty';
import TodayItem from './TodayItem';
import './Paginator.scss';
export default function Paginator(props) {
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
const n = events.length;
setNumEvents(n);
// how many paginated views
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);
setPage(events.slice(eventStart, eventEnd));
// if array is completely in past, show depending on SCROLL_PAST
}, [events, selPage, LIMIT_PER_PAGE]);
// every SCROLL_TIME go to the next array
useInterval(() => {
if (numEvents > LIMIT_PER_PAGE) {
const next = (selPage + 1) % pages;
setSelPage(next);
}
}, SCROLL_TIME);
let selectedState = 0;
if (events?.length < 1) {
return <Empty text='No events to show' />;
}
return (
<div className='paginator entries'>
{page.map((e) => {
if (e.id === selectedId) selectedState = 1;
else if (selectedState === 1) selectedState = 2;
return (
<TodayItem
key={e.id}
selected={selectedState}
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,
};
@@ -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,10 +1,12 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import style from './ProtectRoute.module.scss';
import { PinInput, PinInputField } from '@chakra-ui/react';
import { 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 }) {
const isLocal =
@@ -13,52 +15,75 @@ export default function ProtectRoute({ children }) {
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);
}
};
// 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;
}
return (
<>
{!isLocal && !auth ? (
<div className={style.container}>
ontime
<div className={failed ? style.pin__failed : style.pin}>
<PinInput
type='alphanumeric'
size='lg'
mask
onChange={(value) => {
setFailed(false);
setPin(value);
}}
>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
<IconButton
aria-label='Enter'
size='lg'
isRound
icon={<FiCheck />}
style={{ fontSize: '1.5em' }}
onClick={() => handleValidation()}
/>
</div>
</div>
) : (
children
)}
</>
<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 />}
onClick={() => handleValidation()}
/>
</HStack>
</div>
);
}
@@ -1,8 +1,8 @@
@use '../../../styles/main' as *;
@use '../../../styles/mixins' as *;
@use '../../../theme/main' as *;
@use '../../../theme/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 {

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