diff --git a/.deepsource.toml b/.deepsource.toml index 6ba575648..ce1d28d5f 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -3,7 +3,6 @@ version = 1 test_patterns = [ "__mocks__/**", "__tests__/**", - "cypress/**", "*.test.*", "*.mock.*", "*.spec.*" diff --git a/.eslintrc b/.eslintrc index 389d1e47f..44af6bf47 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,26 +9,18 @@ "extends": [ "eslint:recommended" ], + "overrides": [ + { + "files": [ + "e2e/**/**.spec.ts", + "e2e/**/**.test.ts" + ], + "extends": [ + "plugin:playwright/playwright-test" + ] + } + ], "rules": { - // disallow certain object properties - // https://eslint.org/docs/rules/no-restricted-properties - "no-restricted-properties": [ - "error", - { - "object": "global", - "property": "isNaN", - "message": "Please use Number.isNaN instead" - }, - { - "object": "self", - "property": "isNaN", - "message": "Please use Number.isNaN instead" - }, - { - "object": "window", - "property": "isNaN", - "message": "Please use Number.isNaN instead" - } - ] + "no-console": "warn" } } \ No newline at end of file diff --git a/.github/02_screentypes.png b/.github/aux-images/02_screentypes.png similarity index 100% rename from .github/02_screentypes.png rename to .github/aux-images/02_screentypes.png diff --git a/.github/app.jpg b/.github/aux-images/app.jpg similarity index 100% rename from .github/app.jpg rename to .github/aux-images/app.jpg diff --git a/.github/dockerhub.png b/.github/aux-images/dockerhub.png similarity index 100% rename from .github/dockerhub.png rename to .github/aux-images/dockerhub.png diff --git a/.github/linux-download.png b/.github/aux-images/linux-download.png similarity index 100% rename from .github/linux-download.png rename to .github/aux-images/linux-download.png diff --git a/.github/mac-download.png b/.github/aux-images/mac-download.png similarity index 100% rename from .github/mac-download.png rename to .github/aux-images/mac-download.png diff --git a/.github/social-preview.png b/.github/aux-images/social-preview.png similarity index 100% rename from .github/social-preview.png rename to .github/aux-images/social-preview.png diff --git a/.github/win-download.png b/.github/aux-images/win-download.png similarity index 100% rename from .github/win-download.png rename to .github/aux-images/win-download.png diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00192684a..cf175fe3f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,14 +2,12 @@ name: Ontime build on: push: - # run when a tag is created - tags: - - '*' + tags: [ "v1.*.*" ] workflow_dispatch: jobs: build_mac: - runs-on: macOS-latest + runs-on: macos-latest timeout-minutes: 20 env: CI: '' @@ -138,54 +136,4 @@ jobs: 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 - platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/build_docker_v2.yml b/.github/workflows/build_docker_v2.yml new file mode 100644 index 000000000..33ff92993 --- /dev/null +++ b/.github/workflows/build_docker_v2.yml @@ -0,0 +1,56 @@ +name: Docker Image CI Ontime V2 + +on: + push: + tags: [ "v2.*.*" ] + workflow_dispatch: + +jobs: + + publish_docker: + runs-on: ubuntu-latest + env: + CI: '' + + steps: + - uses: actions/checkout@v3 + - name: Setup env + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Setup Node.js environment + uses: actions/setup-node@v3.6.0 + with: + version: 16.16.0 + + - name: Setup pnpm + uses: pnpm/action-setup@v2.2.4 + with: + version: 7.26.3 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build project packages + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: pnpm turbo build:docker + + - name: Docker Login + uses: docker/login-action@v2.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker Setup Buildx + uses: docker/setup-buildx-action@v2.5.0 + + - name: Build and push Docker images + uses: docker/build-push-action@v4.0.0 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 + # Push is a shorthand for --output=type=registry + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:beta_${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:beta_v2 + diff --git a/.github/workflows/build_v2.yml b/.github/workflows/build_v2.yml new file mode 100644 index 000000000..4d9488bdd --- /dev/null +++ b/.github/workflows/build_v2.yml @@ -0,0 +1,106 @@ +name: Ontime build v2 + +on: + push: + tags: [ "v2.*.*" ] + workflow_dispatch: + +jobs: + build_macos: + runs-on: macOS-latest + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: Setup pnpm + uses: pnpm/action-setup@v2.2.4 + with: + version: 7.26.3 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build project packages + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: pnpm build + + - name: Electron - Build app + run: pnpm dist-mac + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: './apps/electron/dist/ontime-macOS.dmg' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build_windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: Setup pnpm + uses: pnpm/action-setup@v2.2.4 + with: + version: 7.26.3 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build project packages + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: pnpm build + + - name: Electron - Build app + run: pnpm dist-win + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: './apps/electron/dist/ontime-win64.exe' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build_ubuntu: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: Setup pnpm + uses: pnpm/action-setup@v2.2.4 + with: + version: 7.26.3 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build project packages + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: pnpm build + + - name: Electron - Build app + run: pnpm dist-linux + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: './apps/electron/dist/ontime-linux.AppImage' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 4f2f0b8ef..000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,70 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '33 02 * * 5' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/ontime_cy.yml b/.github/workflows/ontime_cy.yml index 2d6734373..627e4d693 100644 --- a/.github/workflows/ontime_cy.yml +++ b/.github/workflows/ontime_cy.yml @@ -3,7 +3,10 @@ name: ontime_test_CI -on: [push, pull_request] +on: + pull_request: + branches: [ master ] + workflow_dispatch: jobs: build: @@ -14,11 +17,11 @@ jobs: CI: '' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: - node-version: '14.x' + node-version: 16 # React - name: React - Install dependencies @@ -40,15 +43,24 @@ jobs: # App - name: Electron - Install dependencies - run: yarn install && yarn setdb + run: yarn setup working-directory: ./server - - name: Electron - Run tests + - name: Server - run tests run: yarn test working-directory: ./server - - name: Cypress run - uses: cypress-io/github-action@v2 - with: - working-directory: ./server - start: yarn cypress +# - name: Install Playwright Browsers +# run: npx playwright install --with-deps +# working-directory: ./server +# +# - name: Run Playwright tests +# run: yarn e2e +# working-directory: ./server +# +# - uses: actions/upload-artifact@v3 +# if: always() +# with: +# name: playwright-report +# path: playwright-report/ +# retention-days: 7 diff --git a/.github/workflows/test_v2.yml b/.github/workflows/test_v2.yml new file mode 100644 index 000000000..48980776d --- /dev/null +++ b/.github/workflows/test_v2.yml @@ -0,0 +1,76 @@ +name: Ontime test v2 + +on: + pull_request: + branches: [ v2 ] + workflow_dispatch: + +jobs: + unit-test: + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + CI: '' + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: Setup pnpm + uses: pnpm/action-setup@v2.2.4 + with: + version: 7.26.3 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # We choose to run tests separately + - name: React - Run unit tests + run: pnpm test:pipeline + working-directory: ./apps/client + + - name: Server - Run unit tests + run: pnpm test:pipeline + working-directory: ./apps/server + + - name: Utils - Run unit tests + run: pnpm test:pipeline + working-directory: ./packages/utils + + e2e-test: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: Setup pnpm + uses: pnpm/action-setup@v2.2.4 + with: + version: 7.26.3 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build client + run: pnpm build:local + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: pnpm e2e + + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index f862b6075..07063a3a5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,38 +5,42 @@ node_modules/ /.pnp .pnp.js -# testing -coverage/ -*.mp4 - -# production -build/ -dist/ - # misc .DS_Store -.env -.env.local -.env.development.local -.env.test.local -.env.production.local +*.local npm-debug.log* yarn-debug.log* yarn-error.log* -# working stuff -_SS/ -db backup.json -server/src/preloaded-db/db.json -server/src/models/db.json -TODO.md -ontime-db/ -ontime-external/ - # vscode stuff .vscode/* ontime.code-workspace # webstorm stuff -.idea/* \ No newline at end of file +.idea/* + +# turborepo stuff +.turbo + +# testing +test-results +playwright-report +/playwright/.cache/ + +# production +build/ +dist/ + +# working stuff +**/TODO.md + +# docker utils +ontime-db +ontime-external/ + +# working database +apps/server/src/preloaded-db/db.json + +# versioning file +**/ONTIME_VERSION.js diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..f272a5417 Binary files /dev/null and b/.nvmrc differ diff --git a/.prettierrc b/.prettierrc index 96e88733c..2a186374a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,8 +1,8 @@ { - "trailingComma": "es5", + "trailingComma": "all", "tabWidth": 2, "semi": true, "singleQuote": true, "jsxSingleQuote": true, - "printWidth": 100 -} \ No newline at end of file + "printWidth": 120 +} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 000000000..2c19cae2d --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,69 @@ +# GETTING STARTED + +Ontime consists of 3 distinct parts +- __client__: A React app for Ontime's UI and web clients +- __electron__: An electron app which facilitates the cross-platform distribution of Ontime +- __server__: A node application which handles the domains services and integrations + +The steps below will assume you have locally installed the necessary dependencies. +Other dependencies will be installed as part of the setup +- __node__ (>=16.16) +- __pnpm__ (>=7) +- __docker__ (only necessary to run and build docker images) + +## LOCAL DEVELOPMENT + +The electron app is only necessary to distribute an installable version of the app and is not required for local development. +Locally, we would need to run both the React client and the node.js server in development mode + +From the project root, run the following commands +- __Install the project dependencies__ by running `pnpm i` +- __Run dev mode__ by running `turbo dev` + +## TESTING + +Generally we have 2 types of tests. +- Unit tests for functions that contain business logic +- End-to-end tests for core features + +### Unit tests +Unit tests are contained in mostly all the apps and packages (client, server and utils) + +You can run unit tests by running turbo `turbo test:pipeline` from the project root. +This will run all tests and close test runner. + +Alternatively you can navigate to an app or project and run `pnpm test` to run those tests in watch mode + +### E2E tests +E2E tests are in a separate package. On running, [playwright](https://playwright.dev/) will spin up an instance of the webserver to test against +These tests also run against a separate version of the DB (test-db) + +You can run playwright tests from project root with `pnpm e2e` + +When writing tests, it can be handy to run playwright in interactive mode with `pnpm e2e:i`. You would need to manually start the webserver with `pnpm dev:server` + +## CREATE AN INSTALLABLE FILE (Windows | MacOS | Linux) + +Ontime uses Electron to distribute the application. +You can generate a distribution for your OS by running the following steps. + +From the project root, run the following commands +- __Install the project dependencies__ by running `pnpm i` +- __Build the UI and server__ by running `turbo build:local` +- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux` + +The build distribution assets will be at `.apps/electron/dist` + +## DOCKER + +Ontime provides a docker-compose file to aid with building and running docker images. +While it should allow for a generic setup, it might need to be modified to fit your infrastructure. + +From the project root, run the following commands +- __Install the project dependencies__ by running `pnpm i` +- __Build docker image from__ by running `docker build -t getontime/ontime` +- __Run docker image from compose__ by running `docker-compose up -d` + +Other useful commands +- __List running processes__ by running `docker ps` +- __Kill running process__ by running `docker kill ` diff --git a/Dockerfile b/Dockerfile index 81669ade1..65df28fe3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,27 @@ -FROM node:14-alpine +FROM node:16-alpine -WORKDIR /app/server +# Set environment variables +# Environment Variable to signal that we are running production +ENV NODE_ENV=docker +# Ontime Data path +ENV ONTIME_DATA=/external/ + +WORKDIR /app/ # Prepare UI -COPY /client/build ../client/build +COPY /apps/client/build ./client/ # Prepare Backend -COPY /server/src ./ +COPY /apps/server/dist/ ./server/ +COPY /demo-db/ ./preloaded-db/ +COPY /apps/server/src/external/ ./external/ -# Export default ports Main - OSC IN -EXPOSE 4001/tcp 8888/udp -ENV NODE_ENV=production -ENV ONTIME_DATA=/server/ +# Export default ports +EXPOSE 4001/tcp 8888/udp 9999/udp -CMD ["yarn", "start:headless"] +CMD ["node", "server/docker.cjs"] -# 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 +# Build and run commands +# !!! Note that this command needs pre-build versions of the UI and server apps +# docker buildx build . -t getontime/ontime +# docker run -p 4001:4001 -p 8888:8888/udp -p 9999:9999/udp -v ./ontime-db:/external/db/ -v ./ontime-styles:/external/styles/ getontime/ontime diff --git a/README.md b/README.md index 0dd58db43..f0d5a4dbd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![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/) +[![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://ontime.gitbook.io) ## Download the latest releases here @@ -14,120 +14,115 @@ 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. +A single, locally hosted central application distributes your event information over the local network. +This enables the distribution of the data to a series of views and allows integration into video and control workflows, including OBS and d3. -![App Window](https://github.com/cpvalente/ontime/blob/master/.github/app.jpg) +![App Window](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/app.jpg) -![Views](https://github.com/cpvalente/ontime/blob/master/.github/02_screentypes.png) +![Views](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/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. +Once installed and running, Ontime starts a background server that is the heart of all processes. +From the app, you can add / edit your running order and control the timer playback. -From here, any device in the same network with a browser is able to render the views as described. +Any device with a browser in the same network can choose one of the supported views to render the available data. 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). +
+You can then use the Ontime logo in the top left corner to select the desired view. +The logo will be initially hidden until there is mouse interaction. -In 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 the case of unattended machines or automation, it is possible to use different URL to recall +individual views and extend view settings using the URL aliases feature ``` -For the presentation views... +For the presentation views ------------------------------------------------------------- IP.ADDRESS:4001 > Web server default to presenter timer view IP.ADDRESS:4001/timer > Presenter / Stage timer view +IP.ADDRESS:4001/minimal > Simple timer view +IP.ADDRESS:4001/clock > Simple clock view IP.ADDRESS:4001/sm > Stage Manager / Backstage view IP.ADDRESS:4001/public > Public / Foyer view -IP.ADDRESS:4001/pip > Picture in Picture view IP.ADDRESS:4001/lower > Lower Thirds IP.ADDRESS:4001/studio > Studio Clock -IP.ADDRESS:4001/cuesheet > Cue Sheet - -...and for the editor (the control interface, same as the app) +``` +``` +For management views ------------------------------------------------------------- -IP.ADDRESS:4001/editor - +IP.ADDRESS:4001/editor > the control interface, same as the app +IP.ADDRESS:4001/cuesheet > realtime cuesheets for collaboration ``` -More documentation available [here](https://cpvalente.gitbook.io/ontime/) +More documentation is available [in our docs](https://ontime.gitbook.io) ## Feature List (in no specific order) -- [x] Distribute Data over network and render in the browser +- [x] Distribute data over network and render it in the browser - [x] Different screen types - Stage Timer - Backstage Info - Public Info - - Picture in Picture - Studio Clock + - Countdown - [Make your own?](#make-your-own-viewer) -- [x] Configurable realtime Lower Thirds -- [x] Cuesheets with additional custom fields +- [x] Configurable Lower Thirds +- [x] Cuesheets with user definable 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] Differentiate between backstage and public data +- [x] Workflow for managing delays +- [x] Protocol integrations for Control and Feedback + - OSC (Open Sound Control) + - HTTP + - WebSockets - [x] Roll mode: run independently using the system clock -- [x] Import event list from Excel -- [x] URL Aliases (define configurable aliases to ease onsite setup) -- [x] Logging view -- [x] Edit anywhere: run ontime in your local network and use any machine to reach the editor page ( - same as app) -- [x] Multi platform (available on Windows, MacOS and Linux) -- [x] [Headless run](#headless-run) (run server only, configure from a browser locally) -- [x] [Countdown to anything!](https://cpvalente.gitbook.io/ontime/views/countdown): ability to have +- [x] [Headless run](#headless-run): run server in a separate machine, configure from a browser locally +- [x] [Countdown to anything!](https://ontime.gitbook.io/v2/views/countdown): have a countdown to any scheduled event +- [x] Multi-platform (available on Windows, MacOS and Linux) +- [x] [Companion integration](https://bitfocus.io/connections/getontime-ontime) ## Unopinionated -We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way +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] If you want just the info screens, there is no need to use the timer! - [x] Don't have or care for a schedule? - - [x] a single event with no data is enough to use the OSC API and get going + - [x] a single event with no data is enough to use one of the APIs and use a dynamic timer - [x] use the order list to create a set of quick timers by setting the beginning and start - times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC as - always + times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC or any of the other available integrations ## Rich APIs for workflow integrations -The app is being currently developed to a wide user base, from broadcast to entertainment and +The app is currently being developed for a broad user base, from broadcast to entertainment and conference halls. -Taking advantage of the integrations in Ontime, we currently use Ontime with: +Taking advantage of the integrations, we currently use Ontime with: -- `disguise`: trigger ontime from d3's timeline using the **OSC API**, **render views** using d3's +- `disguise`: trigger Ontime from d3's timeline using the **OSC API**, and **render views** using d3's webmodule - `OBS`: **render views** using the Browser Module -- `QLab`: trigger ontime using **OSC API** +- `QLab`: trigger Ontime 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) + 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). +Ontime broadcasts its data over WebSockets. This allows you to consume its data outside the application. -See [this repository](https://github.com/cpvalente/ontime-viewer-template) with a small template on +Writing a new view for the browser can be done with 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-v2) 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) +the [Websocket API](https://ontime.gitbook.io/v2/control-and-feedback/websocket-api) ### Headless run️ -You can self host and run ontime in a docker image, the run command should: +You can self-host and run Ontime in a docker image. The run command will: -- expose the necessary ports (listen in Dockerfile) +- expose the necessary ports (listed in the Dockerfile) - mount a local file to persist your data (in the example: ````$(pwd)/local-data````) - the image name __getontime/ontime__ @@ -138,13 +133,7 @@ in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime 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 +and use the included docker compose to get started ```bash docker-compose up @@ -154,19 +143,14 @@ docker-compose up ### 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 +Several features are planned in the roadmap, and we continuously adjust this to match how users interact with the app. +
+Have an idea? Reach out via [email](mail@getontime.no) or [open an issue](https://github.com/cpvalente/ontime/issues/new) ### Issues -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 +We use Github's issue tracking for bug reporting and feature requests.
+Found a bug? [Open an issue](https://github.com/cpvalente/ontime/issues/new). #### Unsigned App @@ -192,22 +176,28 @@ You can circumvent this by allowing the execution of the app manually. Long story short: Ontime app is unsigned.
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) +have tips on how to improve this or would like to sponsor the code signing, +please [open an issue](https://github.com/cpvalente/ontime/issues/new) #### Safari -There are some issues with Safari versions lower than 13: - +There are known 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. +There is no plan for any further work on this. + +# Contributing + +Looking to contribute? All types of help are appreciated, from coding to testing and feature specification. +

+If you are a developer and would like to contribute with some code, please open an issue to discuss before opening a Pull Request. +
+Information about the project setup can be found in the [development documentation](./DEVELOPMENT.md) # Help -Help is underway! ... and can be viewed [here](https://cpvalente.gitbook.io/ontime/) +Help is underway! ... and can be found [here](https://ontime.gitbook.io) # License diff --git a/client/.eslintignore b/apps/client/.eslintignore similarity index 100% rename from client/.eslintignore rename to apps/client/.eslintignore diff --git a/client/.eslintrc b/apps/client/.eslintrc similarity index 88% rename from client/.eslintrc rename to apps/client/.eslintrc index 27e0ad027..402e99e49 100644 --- a/client/.eslintrc +++ b/apps/client/.eslintrc @@ -8,18 +8,32 @@ "browser": true, "node": true }, + "parser": "@typescript-eslint/parser", "extends": [ "eslint:recommended", "plugin:react/recommended", + "plugin:react-hooks/recommended", "plugin:@typescript-eslint/recommended", - "eslint-config-prettier" + "eslint-config-prettier", + "plugin:@tanstack/eslint-plugin-query/recommended", + "prettier" ], "plugins": [ "react", "testing-library", - "simple-import-sort" + "simple-import-sort", + "@tanstack/query", + "@typescript-eslint", + "prettier" ], "rules": { + "@typescript-eslint/no-non-null-assertion": "warn", + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ], "no-useless-concat": "warn", "prefer-template": "warn", "react/jsx-no-bind": [ diff --git a/apps/client/.prettierrc b/apps/client/.prettierrc new file mode 100644 index 000000000..2a186374a --- /dev/null +++ b/apps/client/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "printWidth": 120 +} diff --git a/client/.stylelintrc b/apps/client/.stylelintrc similarity index 100% rename from client/.stylelintrc rename to apps/client/.stylelintrc diff --git a/client/index.html b/apps/client/index.html similarity index 94% rename from client/index.html rename to apps/client/index.html index 4a2721dee..c6a234f36 100644 --- a/client/index.html +++ b/apps/client/index.html @@ -32,6 +32,6 @@
- + diff --git a/apps/client/package.json b/apps/client/package.json new file mode 100644 index 000000000..aadc5ed14 --- /dev/null +++ b/apps/client/package.json @@ -0,0 +1,99 @@ +{ + "name": "ontime-ui", + "version": "2.0.0-beta", + "private": true, + "dependencies": { + "@chakra-ui/react": "^2.5.5", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@react-icons/all-files": "^4.1.0", + "@sentry/react": "^7.46.0", + "@sentry/tracing": "^7.46.0", + "@tanstack/react-query": "^4.28.0", + "@tanstack/react-query-devtools": "^4.29.0", + "autosize": "^5.0.2", + "axios": "^1.2.0", + "color": "^4.2.3", + "csv-stringify": "^6.2.3", + "deepmerge": "^4.3.0", + "framer-motion": "^10.10.0", + "react": "^18.2.0", + "react-colorful": "^5.6.1", + "react-dom": "^18.2.0", + "react-fast-compare": "^3.2.0", + "react-hook-form": "^7.43.5", + "react-qr-code": "^2.0.11", + "react-router-dom": "^6.3.0", + "react-table": "^7.7.0", + "typeface-open-sans": "^1.1.13", + "web-vitals": "^3.1.1", + "zustand": "^4.3.6" + }, + "scripts": { + "addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js", + "postinstall": "pnpm addversion", + "dev": "cross-env BROWSER=none vite", + "build": "vite build", + "build:local": "cross-env NODE_ENV=local vite build", + "build:docker": "vite build", + "lint": "eslint .", + "stylelint": "npx stylelint \"**/*.scss\"\n", + "test": "vitest", + "test:pipeline": "vitest run", + "cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@sentry/vite-plugin": "^0.4.0", + "@tanstack/eslint-plugin-query": "^4.26.2", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.1.1", + "@testing-library/user-event": "^14.1.1", + "@types/color": "^3.0.3", + "@types/luxon": "^3.2.0", + "@types/prop-types": "^15.7.5", + "@types/react": "^18.0.26", + "@types/react-dom": "^18.0.10", + "@types/testing-library__jest-dom": "^5.14.5", + "@typescript-eslint/eslint-plugin": "^5.48.1", + "@typescript-eslint/parser": "^5.48.1", + "@vitejs/plugin-react": "^3.0.1", + "eslint": "^8.31.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-jest": "^27.1.7", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.32.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-simple-import-sort": "^8.0.0", + "eslint-plugin-testing-library": "^5.9.1", + "jsdom": "^21.1.0", + "ontime-types": "workspace:*", + "ontime-utils": "workspace:*", + "prettier": "^2.8.3", + "prop-types": "^15.8.1", + "sass": "^1.57.1", + "stylelint": "^14.16.1", + "stylelint-config-prettier": "^9.0.4", + "stylelint-config-standard-scss": "^6.1.0", + "typescript": "^4.9.4", + "vite": "^4.3.1", + "vite-plugin-compression2": "^0.9.0", + "vite-plugin-svgr": "^2.4.0", + "vite-tsconfig-paths": "^4.2.0", + "vitest": "^0.30.1" + } +} diff --git a/client/public/android-chrome-192x192.png b/apps/client/public/android-chrome-192x192.png similarity index 100% rename from client/public/android-chrome-192x192.png rename to apps/client/public/android-chrome-192x192.png diff --git a/client/public/android-chrome-512x512.png b/apps/client/public/android-chrome-512x512.png similarity index 100% rename from client/public/android-chrome-512x512.png rename to apps/client/public/android-chrome-512x512.png diff --git a/client/public/apple-touch-icon.png b/apps/client/public/apple-touch-icon.png similarity index 100% rename from client/public/apple-touch-icon.png rename to apps/client/public/apple-touch-icon.png diff --git a/client/public/favicon-16x16.png b/apps/client/public/favicon-16x16.png similarity index 100% rename from client/public/favicon-16x16.png rename to apps/client/public/favicon-16x16.png diff --git a/client/public/favicon-32x32.png b/apps/client/public/favicon-32x32.png similarity index 100% rename from client/public/favicon-32x32.png rename to apps/client/public/favicon-32x32.png diff --git a/client/public/favicon.ico b/apps/client/public/favicon.ico similarity index 100% rename from client/public/favicon.ico rename to apps/client/public/favicon.ico diff --git a/client/public/logo192.png b/apps/client/public/logo192.png similarity index 100% rename from client/public/logo192.png rename to apps/client/public/logo192.png diff --git a/client/public/logo512.png b/apps/client/public/logo512.png similarity index 100% rename from client/public/logo512.png rename to apps/client/public/logo512.png diff --git a/client/public/manifest.json b/apps/client/public/manifest.json similarity index 100% rename from client/public/manifest.json rename to apps/client/public/manifest.json diff --git a/client/public/robots.txt b/apps/client/public/robots.txt similarity index 100% rename from client/public/robots.txt rename to apps/client/public/robots.txt diff --git a/client/public/site.webmanifest b/apps/client/public/site.webmanifest similarity index 100% rename from client/public/site.webmanifest rename to apps/client/public/site.webmanifest diff --git a/client/src/App.jsx b/apps/client/src/App.tsx similarity index 52% rename from client/src/App.jsx rename to apps/client/src/App.tsx index 37365f3ea..e2b8b247d 100644 --- a/client/src/App.jsx +++ b/apps/client/src/App.tsx @@ -1,24 +1,29 @@ -import { Suspense, useCallback, useEffect } from 'react'; +import { useEffect } from 'react'; import { BrowserRouter } from 'react-router-dom'; import { ChakraProvider } from '@chakra-ui/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary'; +import ErrorBoundary from './common/components/error-boundary/ErrorBoundary'; import { AppContextProvider } from './common/context/AppContext'; -import SocketProvider from './common/context/socketContext'; import useElectronEvent from './common/hooks/useElectronEvent'; +import { ontimeQueryClient } from './common/queryClient'; +import { connectSocket } from './common/utils/socket'; import theme from './theme/theme'; +import { TranslationProvider } from './translation/TranslationProvider'; import AppRouter from './AppRouter'; // Load Open Sans typeface +// @ts-expect-error no types from font import import('typeface-open-sans'); -export const ontimeQueryClient = new QueryClient(); + +connectSocket(); function App() { const { isElectron, sendToElectron } = useElectronEvent(); - const handleKeyPress = useCallback((event) => { + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { // handle held key if (event.repeat) return; // check if the alt key is pressed @@ -28,9 +33,8 @@ function App() { sendToElectron('set-window', 'show-dev'); } } - },[]); + }; - useEffect(() => { if (isElectron) { document.addEventListener('keydown', handleKeyPress); } @@ -39,26 +43,24 @@ function App() { document.removeEventListener('keydown', handleKeyPress); } }; - }, [handleKeyPress]); + }, [isElectron, sendToElectron]); return ( - - - - -
- - - - - - -
-
-
-
-
+ + + +
+ + + + + + +
+
+
+
); } diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx new file mode 100644 index 000000000..a71c3e945 --- /dev/null +++ b/apps/client/src/AppRouter.tsx @@ -0,0 +1,121 @@ +import { lazy, Suspense, useEffect } from 'react'; +import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; + +import useAliases from './common/hooks-query/useAliases'; +import withData 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 StudioClock = lazy(() => import('./features/viewers/studio/StudioClock')); + +const STimer = withData(TimerView); +const SMinimalTimer = withData(MinimalTimerView); +const SClock = withData(ClockView); +const SCountdown = withData(Countdown); +const SBackstage = withData(Backstage); +const SPublic = withData(Public); +const SLowerThird = withData(Lower); +const SStudio = withData(StudioClock); + +const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper')); +const RundownPanel = lazy(() => import('./features/rundown/RundownExport')); +const TimerControl = lazy(() => import('./features/control/playback/TimerControlExport')); +const MessageControl = lazy(() => import('./features/control/message/MessageControlExport')); +const Info = lazy(() => import('./features/info/InfoExport')); + +export default function AppRouter() { + const { data } = useAliases(); + 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 ( + + + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + + } /> + + } /> + + } /> + } /> + + } /> + } /> + {/*/!* Lower cannot have fallback *!/*/} + } /> + + {/*/!* Protected Routes *!/*/} + } /> + } /> + } /> + } /> + + {/*/!* Protected Routes - Elements *!/*/} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + {/*/!* Send to default if nothing found *!/*/} + } /> + + + ); +} diff --git a/client/src/__mocks__/QueryClient.mock.js b/apps/client/src/__mocks__/QueryClient.mock.js similarity index 100% rename from client/src/__mocks__/QueryClient.mock.js rename to apps/client/src/__mocks__/QueryClient.mock.js diff --git a/apps/client/src/appConstants.ts b/apps/client/src/appConstants.ts new file mode 100644 index 000000000..a44531dca --- /dev/null +++ b/apps/client/src/appConstants.ts @@ -0,0 +1,22 @@ +// Exported viewer link location +const minimalLocation = 'minimal'; +const speakerLocation = 'speaker'; +const smLocation = 'sm'; +const publicLocation = 'public'; +const studioLocation = 'studio'; +const cuesheetLocation = 'cuesheet'; +const countdownLocation = 'countdown'; +const clockLocation = 'clock'; +const lowerLocation = 'lower'; + +export const viewerLocations = [ + { link: speakerLocation, label: 'Stage timer' }, + { link: clockLocation, label: 'Clock' }, + { link: minimalLocation, label: 'Minimal timer' }, + { link: smLocation, label: 'Backstage screen' }, + { link: publicLocation, label: 'Public screen' }, + { link: lowerLocation, label: 'Lower thirds' }, + { link: studioLocation, label: 'Studio clock' }, + { link: countdownLocation, label: 'Countdown' }, + { link: cuesheetLocation, label: 'Cuesheet' }, +]; diff --git a/client/src/assets/fonts/digital-7.monoitalic.ttf b/apps/client/src/assets/fonts/digital-7.monoitalic.ttf similarity index 100% rename from client/src/assets/fonts/digital-7.monoitalic.ttf rename to apps/client/src/assets/fonts/digital-7.monoitalic.ttf diff --git a/client/src/assets/images/empty.svg b/apps/client/src/assets/images/empty.svg similarity index 100% rename from client/src/assets/images/empty.svg rename to apps/client/src/assets/images/empty.svg diff --git a/apps/client/src/assets/images/ontime-logo.svg b/apps/client/src/assets/images/ontime-logo.svg new file mode 100644 index 000000000..41594e5e6 --- /dev/null +++ b/apps/client/src/assets/images/ontime-logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/client/src/common/api/apiConstants.ts b/apps/client/src/common/api/apiConstants.ts new file mode 100644 index 000000000..36bf11324 --- /dev/null +++ b/apps/client/src/common/api/apiConstants.ts @@ -0,0 +1,24 @@ +export const STATIC_PORT = 4001; + +// REST stuff +export const EVENT_DATA = ['eventdata']; +export const ALIASES = ['aliases']; +export const USERFIELDS = ['userFields']; +export const RUNDOWN_TABLE_KEY = 'rundown'; +export const RUNDOWN_TABLE = [RUNDOWN_TABLE_KEY]; +export const APP_INFO = ['appinfo']; +export const OSC_SETTINGS = ['oscSettings']; +export const APP_SETTINGS = ['appSettings']; +export const VIEW_SETTINGS = ['viewSettings']; +export const RUNTIME = ['runtimeStore']; + +export const serverPort = import.meta.env.DEV ? STATIC_PORT : window.location.port; +export const serverURL = import.meta.env.DEV ? `http://localhost:${serverPort}` : window.location.origin; +export const websocketUrl = `ws://${window.location.hostname}:${serverPort}/ws`; + +export const eventURL = `${serverURL}/eventdata`; +export const rundownURL = `${serverURL}/events`; +export const ontimeURL = `${serverURL}/ontime`; + +export const stylesPath = 'external/styles/override.css'; +export const overrideStylesURL = `${serverURL}/${stylesPath}`; diff --git a/client/src/common/api/eventApi.js b/apps/client/src/common/api/eventDataApi.ts similarity index 59% rename from client/src/common/api/eventApi.js rename to apps/client/src/common/api/eventDataApi.ts index d59335b7c..9e29750ea 100644 --- a/client/src/common/api/eventApi.js +++ b/apps/client/src/common/api/eventDataApi.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { EventData } from 'ontime-types'; import { eventURL } from './apiConstants'; @@ -6,13 +7,15 @@ import { eventURL } from './apiConstants'; * @description HTTP request to fetch event data * @return {Promise} */ -export const fetchEvent = async () => { +export async function fetchEventData(): Promise { 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); +export async function postEventData(data: EventData) { + return axios.post(eventURL, data); +} diff --git a/apps/client/src/common/api/eventsApi.ts b/apps/client/src/common/api/eventsApi.ts new file mode 100644 index 000000000..fd8c406bd --- /dev/null +++ b/apps/client/src/common/api/eventsApi.ts @@ -0,0 +1,75 @@ +import axios from 'axios'; +import { OntimeRundown, OntimeRundownEntry } from 'ontime-types'; + +import { rundownURL } from './apiConstants'; + +/** + * @description HTTP request to fetch all events + * @return {Promise} + */ +export async function fetchRundown(): Promise { + const res = await axios.get(rundownURL); + return res.data; +} + +/** + * @description HTTP request to post new event + * @return {Promise} + */ +export async function requestPostEvent(data: OntimeRundownEntry) { + return axios.post(rundownURL, data); +} + +/** + * @description HTTP request to put new event + * @return {Promise} + */ +export async function requestPutEvent(data: Partial) { + return axios.put(rundownURL, data); +} + +/** + * @description HTTP request to modify event + * @return {Promise} + */ +export async function requestPatchEvent(data: OntimeRundownEntry) { + return axios.patch(rundownURL, data); +} + +export type ReorderEntry = { + eventId: string; + from: number; + to: number; +}; + +/** + * @description HTTP request to reorder events + * @return {Promise} + */ +export async function requestReorderEvent(data: ReorderEntry) { + return axios.patch(`${rundownURL}/reorder`, data); +} + +/** + * @description HTTP request to request application of delay + * @return {Promise} + */ +export async function requestApplyDelay(eventId: string) { + return axios.patch(`${rundownURL}/applydelay/${eventId}`); +} + +/** + * @description HTTP request to delete given event + * @return {Promise} + */ +export async function requestDelete(eventId: string) { + return axios.delete(`${rundownURL}/${eventId}`); +} + +/** + * @description HTTP request to delete all events + * @return {Promise} + */ +export async function requestDeleteAll() { + return axios.delete(`${rundownURL}/all`); +} diff --git a/apps/client/src/common/api/ontimeApi.ts b/apps/client/src/common/api/ontimeApi.ts new file mode 100644 index 000000000..a826d4981 --- /dev/null +++ b/apps/client/src/common/api/ontimeApi.ts @@ -0,0 +1,175 @@ +import axios from 'axios'; +import { Alias, EventData, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types'; + +import { apiRepoLatest } from '../../externals'; +import { InfoType } from '../models/Info'; + +import { ontimeURL } from './apiConstants'; + +/** + * @description HTTP request to retrieve application settings + * @return {Promise} + */ +export async function getSettings(): Promise { + const res = await axios.get(`${ontimeURL}/settings`); + return res.data; +} + +/** + * @description HTTP request to mutate application settings + * @return {Promise} + */ +export async function postSettings(data: Settings) { + return axios.post(`${ontimeURL}/settings`, data); +} + +/** + * @description HTTP request to retrieve application info + * @return {Promise} + */ +export async function getInfo(): Promise { + const res = await axios.get(`${ontimeURL}/info`); + return res.data; +} + +/** + * @description HTTP request to retrieve view settings + * @return {Promise} + */ +export async function getView(): Promise { + const res = await axios.get(`${ontimeURL}/views`); + return res.data; +} + +/** + * @description HTTP request to mutate view settings + * @return {Promise} + */ +export async function postViewSettings(data: ViewSettings) { + return axios.post(`${ontimeURL}/views`, data); +} + +/** + * @description HTTP request to retrieve aliases + * @return {Promise} + */ +export async function getAliases(): Promise { + const res = await axios.get(`${ontimeURL}/aliases`); + return res.data; +} + +/** + * @description HTTP request to mutate aliases + * @return {Promise} + */ +export async function postAliases(data: Alias[]) { + return axios.post(`${ontimeURL}/aliases`, data); +} + +/** + * @description HTTP request to retrieve user fields + * @return {Promise} + */ +export async function getUserFields(): Promise { + const res = await axios.get(`${ontimeURL}/userfields`); + return res.data; +} + +/** + * @description HTTP request to mutate user fields + * @return {Promise} + */ +export async function postUserFields(data: UserFields) { + return axios.post(`${ontimeURL}/userfields`, data); +} + +/** + * @description HTTP request to retrieve osc settings + * @return {Promise} + */ +export async function getOSC(): Promise { + const res = await axios.get(`${ontimeURL}/osc`); + return res.data; +} + +/** + * @description HTTP request to mutate osc settings + * @return {Promise} + */ +export async function postOSC(data: OSCSettings) { + return axios.post(`${ontimeURL}/osc`, data); +} + +/** + * @description HTTP request to download db + * @return {Promise} + */ +export const downloadRundown = async () => { + await axios({ + url: `${ontimeURL}/db`, + method: 'GET', + responseType: 'blob', // important + }).then((response) => { + const headerLine = response.headers['Content-Disposition']; + let filename = 'rundown.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} + */ +type UploadDataOptions = { + onlyRundown?: boolean; +}; +export const uploadData = async (file: File, setProgress: (value: number) => void, options?: UploadDataOptions) => { + const formData = new FormData(); + formData.append('userFile', file); + const onlyRundown = options?.onlyRundown || 'false'; + await axios + .post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + const complete = progressEvent?.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0; + setProgress(complete); + }, + }) + .then((response) => response.data.id); +}; + +export type HasUpdate = { + url: string; + version: string; +}; + +/** + * @description HTTP request to get the latest version and url from github + * @return {Promise} + */ +export async function getLatestVersion(): Promise { + const res = await axios.get(`${apiRepoLatest}`); + return { + url: res.data.html_url as string, + version: res.data.tag_name as string, + }; +} + +export async function postNew(initialData: Partial) { + return axios.post(`${ontimeURL}/new`, initialData); +} diff --git a/client/src/common/components/buttons/EnableBtn.jsx b/apps/client/src/common/components/buttons/EnableBtn.jsx similarity index 94% rename from client/src/common/components/buttons/EnableBtn.jsx rename to apps/client/src/common/components/buttons/EnableBtn.jsx index 8948d5b4d..c9dace127 100644 --- a/client/src/common/components/buttons/EnableBtn.jsx +++ b/apps/client/src/common/components/buttons/EnableBtn.jsx @@ -1,4 +1,4 @@ -import { Button } from '@chakra-ui/button'; +import { Button } from '@chakra-ui/react'; import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp'; import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp'; import PropTypes from 'prop-types'; diff --git a/client/src/common/components/buttons/PauseIconBtn.jsx b/apps/client/src/common/components/buttons/PauseIconBtn.tsx similarity index 61% rename from client/src/common/components/buttons/PauseIconBtn.jsx rename to apps/client/src/common/components/buttons/PauseIconBtn.tsx index 7d80b431d..5bc581a6c 100644 --- a/client/src/common/components/buttons/PauseIconBtn.jsx +++ b/apps/client/src/common/components/buttons/PauseIconBtn.tsx @@ -1,30 +1,28 @@ -import { IconButton } from '@chakra-ui/button'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Tooltip } from '@chakra-ui/react'; import { IoPause } from '@react-icons/all-files/io5/IoPause'; -import PropTypes from 'prop-types'; import { tooltipDelayMid } from '../../../ontimeConfig'; -export default function PauseIconBtn(props) { +interface PauseIconBtnProps { + clickhandler: (event: React.MouseEvent) => void; + active: boolean; + disabled: boolean; +} + +export default function PauseIconBtn(props: PauseIconBtnProps) { const { clickhandler, active, disabled, ...rest } = props; return ( } colorScheme='orange' - _hover={!disabled && { bg: 'orange.400' }} variant={active ? 'solid' : 'outline'} onClick={clickhandler} width={120} disabled={disabled} + aria-label='Pause playback' {...rest} /> ); } - -PauseIconBtn.propTypes = { - clickhandler: PropTypes.func, - active: PropTypes.bool, - disabled: PropTypes.bool -} diff --git a/client/src/common/components/buttons/PublicIconBtn.jsx b/apps/client/src/common/components/buttons/PublicIconBtn.jsx similarity index 88% rename from client/src/common/components/buttons/PublicIconBtn.jsx rename to apps/client/src/common/components/buttons/PublicIconBtn.jsx index 969559b85..c9f9c0374 100644 --- a/client/src/common/components/buttons/PublicIconBtn.jsx +++ b/apps/client/src/common/components/buttons/PublicIconBtn.jsx @@ -1,5 +1,4 @@ -import { IconButton } from '@chakra-ui/button'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Tooltip } from '@chakra-ui/react'; import { FiUsers } from '@react-icons/all-files/fi/FiUsers'; import PropTypes from 'prop-types'; diff --git a/client/src/common/components/buttons/QuitIconBtn.jsx b/apps/client/src/common/components/buttons/QuitIconBtn.tsx similarity index 51% rename from client/src/common/components/buttons/QuitIconBtn.jsx rename to apps/client/src/common/components/buttons/QuitIconBtn.tsx index 7dd1325b2..43b03b85f 100644 --- a/client/src/common/components/buttons/QuitIconBtn.jsx +++ b/apps/client/src/common/components/buttons/QuitIconBtn.tsx @@ -1,5 +1,4 @@ -import { useCallback, useContext, useEffect, useRef, useState } from 'react'; -import { Button, IconButton } from '@chakra-ui/button'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { AlertDialog, AlertDialogBody, @@ -7,24 +6,52 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, -} from '@chakra-ui/modal'; -import { Tooltip } from '@chakra-ui/tooltip'; + Button, + IconButton, + Tooltip, +} from '@chakra-ui/react'; import { FiPower } from '@react-icons/all-files/fi/FiPower'; -import PropTypes from 'prop-types'; -import { LoggingContext } from '../../context/LoggingContext'; +import { Size } from '../../models/Util.type'; +import { useEmitLog } from '../../stores/logger'; -export default function QuitIconBtn(props) { - const { clickHandler, size = 'lg', ...rest } = props; +interface QuitIconBtnProps { + clickHandler: () => void; + size?: Size; + disabled?: boolean; +} + +const quitBtnStyle = { + color: '#D20300', // $red-700 + borderColor: '#D20300', // $red-700 + _focus: { boxShadow: 'none' }, + _hover: { + background: '#D20300', // $red-700 + color: 'white', + _disabled: { + color: '#D20300', // $red-700 + background: 'none', + }, + }, + _active: { + background: '#9A0000', // $red-1000 + color: 'white', + }, + variant: 'outline', + isRound: true, +}; + +export default function QuitIconBtn(props: QuitIconBtnProps) { + const { clickHandler, size = 'lg', disabled, ...rest } = props; const [isOpen, setIsOpen] = useState(false); - const { emitInfo } = useContext(LoggingContext); + const { emitInfo } = useEmitLog(); const onClose = () => setIsOpen(false); - const cancelRef = useRef(); + const cancelRef = useRef(null); useEffect(() => { if (window.process?.type === 'renderer') { window.ipcRenderer.on('user-request-shutdown', () => { - emitInfo('Shutdown request') + emitInfo('Shutdown request'); setIsOpen(true); }); } @@ -39,13 +66,12 @@ export default function QuitIconBtn(props) { <> } - colorScheme='red' - variant='outline' - isRound onClick={() => setIsOpen(true)} - _focus={{ boxShadow: 'none' }} + isDisabled={disabled} + {...quitBtnStyle} {...rest} /> @@ -53,13 +79,11 @@ export default function QuitIconBtn(props) { - Server Shutdown + Ontime Shutdown - - This will shutdown the program and all running servers. Are you sure? - + This will shutdown the program and all running servers. Are you sure? - + } variant='ontime-filled' tabIndex={-1} onClick={handleClick} /> + + + ); +} diff --git a/client/src/common/components/errorBoundary/ErrorBoundary.jsx b/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx similarity index 60% rename from client/src/common/components/errorBoundary/ErrorBoundary.jsx rename to apps/client/src/common/components/error-boundary/ErrorBoundary.jsx index 9f8e31c5e..0fb8bf8fc 100644 --- a/client/src/common/components/errorBoundary/ErrorBoundary.jsx +++ b/apps/client/src/common/components/error-boundary/ErrorBoundary.jsx @@ -1,13 +1,10 @@ /* eslint-disable react/destructuring-assignment */ import React from 'react'; - -import { version as appVersion } from '../../../../package.json'; -import { LoggingContext } from '../../context/LoggingContext'; +import * as Sentry from '@sentry/react'; import style from './ErrorBoundary.module.scss'; class ErrorBoundary extends React.Component { - static contextType = LoggingContext; reportContent = ''; constructor(props) { @@ -25,36 +22,34 @@ class ErrorBoundary extends React.Component { error: error, errorInfo: info, }); - try { - this.context.emitError(error.toString()); - } catch (e) { - console.log('Unable to emit error', error, e ) - } + + Sentry.withScope((scope) => { + scope.setExtras(error); + const eventId = Sentry.captureException(error); + this.setState({ eventId, info }); + }); this.reportContent = `${error} ${info.componentStack}`; } render() { if (this.state.errorMessage) { return ( -
+

:/

Something went wrong

-

{ - if (navigator.clipboard) { - const copyContent = `ontime version ${appVersion} \n ${this.reportContent}`; - navigator.clipboard.writeText(copyContent); - } - }} + onClick={() => Sentry.showReportDialog({ eventId: this.state.eventId })} > - Copy error -

-

+

{ - if (window.process.type === 'renderer') { + if (window?.process?.type === 'renderer') { window.ipcRenderer.send('reload'); } else { window.location.reload(); @@ -62,7 +57,7 @@ class ErrorBoundary extends React.Component { }} > Reload interface -

+
); diff --git a/client/src/common/components/errorBoundary/ErrorBoundary.module.scss b/apps/client/src/common/components/error-boundary/ErrorBoundary.module.scss similarity index 68% rename from client/src/common/components/errorBoundary/ErrorBoundary.module.scss rename to apps/client/src/common/components/error-boundary/ErrorBoundary.module.scss index afde83608..71182baf6 100644 --- a/client/src/common/components/errorBoundary/ErrorBoundary.module.scss +++ b/apps/client/src/common/components/error-boundary/ErrorBoundary.module.scss @@ -1,4 +1,4 @@ -@use '../../../theme/main' as *; +@use '../../../theme/v2Styles' as *; .errorContainer { width: 100%; @@ -9,17 +9,17 @@ color: white; .error { - color: $ontime-pink; + color: $error-red; font-weight: 600; } .report { - text-decoration: underline $ontime-pink; + text-decoration: underline $error-red; cursor: pointer; } .report:hover { - color: $ontime-pink; + color: $error-red; } .report:active { diff --git a/apps/client/src/common/components/input/auto-text-area/AutoTextArea.tsx b/apps/client/src/common/components/input/auto-text-area/AutoTextArea.tsx new file mode 100644 index 000000000..2ed97a970 --- /dev/null +++ b/apps/client/src/common/components/input/auto-text-area/AutoTextArea.tsx @@ -0,0 +1,33 @@ +import { useEffect, useRef } from 'react'; +import { Textarea, TextareaProps } from '@chakra-ui/react'; +// @ts-expect-error no types from library +import autosize from 'autosize/dist/autosize'; + +interface AutoTextAreaProps extends TextareaProps { + isDark?: boolean; +} + +export const AutoTextArea = (props: AutoTextAreaProps) => { + const { isDark, ...rest } = props; + const ref = useRef(null); + + useEffect(() => { + const node = ref.current; + autosize(ref.current); + return () => { + autosize.destroy(node); + }; + }, []); + + return ( +