mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-05 23:43:57 +00:00
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
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,166 @@
|
||||
name: Ontime build
|
||||
|
||||
on:
|
||||
push:
|
||||
# run when a tag is created
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_mac:
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Inject utils
|
||||
run: yarn cheat
|
||||
working-directory: ./server
|
||||
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-mac
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-macOS.dmg
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build_win:
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Inject utils
|
||||
run: yarn cheat
|
||||
working-directory: ./server
|
||||
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-mac
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-win64.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# build_linux:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# CI: ''
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Use Node.js
|
||||
# uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: '14.x'
|
||||
#
|
||||
# Utils server
|
||||
# - name: Utils - Install dependencies
|
||||
# run: yarn install
|
||||
# working-directory: ./server/utils
|
||||
#
|
||||
# - name: Utils - run link command
|
||||
# run: yarn link
|
||||
# working-directory: ./server/utils
|
||||
#
|
||||
# React
|
||||
# - name: React - Link to utils
|
||||
# run: yarn link ontime-utils
|
||||
# working-directory: ./client
|
||||
#
|
||||
# - name: React - Install dependencies
|
||||
# run: yarn install
|
||||
# working-directory: ./client
|
||||
#
|
||||
# - name: React - Build project
|
||||
# run: yarn build
|
||||
# working-directory: ./client
|
||||
#
|
||||
# Node server
|
||||
# - name: React - Link to utils
|
||||
# run: yarn link ontime-utils
|
||||
# working-directory: ./server/src
|
||||
#
|
||||
# - name: Server - Install dependencies
|
||||
# run: yarn install
|
||||
# working-directory: ./server/src
|
||||
#
|
||||
# App
|
||||
# - name: Electron - Install dependencies
|
||||
# run: yarn install && yarn make && yarn setdb
|
||||
# working-directory: ./server
|
||||
# - name: Electron - Inject utils
|
||||
# run: yarn cheat
|
||||
# working-directory: ./server
|
||||
#
|
||||
# - name: Electron - Build app
|
||||
# run: yarn dist-linux
|
||||
# working-directory: ./server
|
||||
#
|
||||
# Release
|
||||
# - name: Release
|
||||
# uses: softprops/action-gh-release@v1
|
||||
# with:
|
||||
# files: ./server/dist/ontime-win64.exe
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -19,20 +19,7 @@ jobs:
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# Utils server
|
||||
- name: Utils - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/utils
|
||||
|
||||
- name: Utils - run link command
|
||||
run: yarn link
|
||||
working-directory: ./server/utils
|
||||
|
||||
# React
|
||||
- name: React - Link to utils
|
||||
run: yarn link ontime-utils
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
@@ -46,17 +33,13 @@ jobs:
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: React - Link to utils
|
||||
run: yarn link ontime-utils
|
||||
working-directory: ./server/src
|
||||
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
run: yarn install && yarn make && yarn setdb
|
||||
run: yarn install && yarn setdb
|
||||
working-directory: ./server
|
||||
|
||||
- name: Electron - Run tests
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
|
||||
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>
|
||||
<img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/>
|
||||
</div>
|
||||
|
||||
# Ontime
|
||||
Ontime is an application for managing event rundowns and running stage timers.
|
||||
@@ -27,7 +31,7 @@ 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
|
||||
@@ -85,25 +89,44 @@ Taking advantage of the integrations in Ontime, we currently use Ontime with:
|
||||
### Continued development
|
||||
There are several features planned in the roadmap.
|
||||
These will be implemented in a development friendly order unless there is user demand to bump any of them.
|
||||
- [ ] HTTP Server (vMix integration)
|
||||
- [ ] Improvement with event component design
|
||||
- [ ] New playback mode for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
|
||||
- [ ] Linux version
|
||||
- [ ] Headless version (run server only anywhere, configure from a browser locally)
|
||||
- [ ] Companion module
|
||||
- [ ] Lower Third Manager
|
||||
- [ ] Note only event
|
||||
- [ ] Reach Schedule: way to speedup timer to meet a deadline
|
||||
- [ ] vMix integration
|
||||
|
||||
### For version 1
|
||||
Almost reaching a feature set that we can call v1. Before that:
|
||||
- [ ] Mac OS version
|
||||
|
||||
### Issues
|
||||
The app is still in pre-release and there are a few issues, mainly concerning style.
|
||||
This will be receiving attention as we near v1 release
|
||||
The app is still in pre-release and there are a few issues, mainly concerning responsiveness in
|
||||
different screens. If you run into problems, please open an issue with a screenshot and your device
|
||||
resolution
|
||||
|
||||
#### Style
|
||||
- [ ] App needs improvement on handling zoomed interfaces: Please ensure that windows settings have no display zoom (it is 125% by default)
|
||||
- [ ] Very long titles might cause interface to shift
|
||||
#### Unsigned App
|
||||
|
||||
When installing the app you would see warning screens from the Operating System like:
|
||||
|
||||
```Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.```
|
||||
or
|
||||
```Ontime can't be opened because it is from an unidentified developer```
|
||||
Long story short: Ontime app is unsigned. </br>Purchasing the certificates for both Mac and Windows
|
||||
incurs an early cost and is not a priority. This is unlikely to change in future.
|
||||
If you have tips on how to improve this, or would like to sponsor the code signing,
|
||||
please [open an issue so we can discuss it](https://github.com/cpvalente/ontime/issues/new)
|
||||
|
||||
#### Safari
|
||||
|
||||
There are some issues with Safari versions lower than 13:
|
||||
- Spacing and text styles do not render correctly
|
||||
- Table view does not work
|
||||
|
||||
There is no plan for any further work on this since the breaking code belongs to third party libraries.
|
||||
|
||||
# Help
|
||||
Help is underway! ... and can be viewed [here](https://cpvalente.gitbook.io/ontime/)
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"axios": "^0.25.0",
|
||||
"color": "^4.2.1",
|
||||
"framer-motion": "4.1.17",
|
||||
"ontime-utils": "link: ../server/utils/",
|
||||
"react": "17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.1",
|
||||
|
||||
@@ -6,7 +6,4 @@ export const fetchEvent = async () => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postEvent = async (data) => {
|
||||
const res = await axios.post(eventURL, data);
|
||||
return res;
|
||||
};
|
||||
export const postEvent = async (data) => axios.post(eventURL, data);
|
||||
|
||||
@@ -6,31 +6,16 @@ export const fetchAllEvents = async () => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const requestPost = async (data) => {
|
||||
await axios.post(eventsURL, data);
|
||||
};
|
||||
export const requestPost = async (data) => axios.post(eventsURL, data);
|
||||
|
||||
export const requestPut = async (data) => {
|
||||
await axios.put(eventsURL, data);
|
||||
};
|
||||
export const requestPut = async (data) => axios.put(eventsURL, data);
|
||||
|
||||
export const requestPatch = async (data) => {
|
||||
await axios.patch(eventsURL, data);
|
||||
};
|
||||
export const requestPatch = async (data) => axios.patch(eventsURL, data);
|
||||
|
||||
export const requestReorder = async (data) => {
|
||||
await axios.patch(`${eventsURL}/reorder`, data);
|
||||
};
|
||||
export const requestReorder = async (data) => axios.patch(`${eventsURL}/reorder`, data);
|
||||
|
||||
export const requestApplyDelay = async (eventId) => {
|
||||
const action = 'applydelay';
|
||||
return await axios.patch(`${eventsURL}/${action}/${eventId}`);
|
||||
};
|
||||
export const requestApplyDelay = async (eventId) => axios.patch(`${eventsURL}/applydelay/${eventId}`);
|
||||
|
||||
export const requestDelete = async (eventId) => {
|
||||
await axios.delete(`${eventsURL}/${eventId}`);
|
||||
};
|
||||
export const requestDelete = async (eventId) => axios.delete(`${eventsURL}/${eventId}`);
|
||||
|
||||
export const requestDeleteAll = async () => {
|
||||
await axios.delete(`${eventsURL}/all`);
|
||||
};
|
||||
export const requestDeleteAll = async () => axios.delete(`${eventsURL}/all`);
|
||||
|
||||
@@ -104,46 +104,35 @@ export const getSettings = async () => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postSettings = async (data) => {
|
||||
await axios.post(`${ontimeURL}/settings`, data);
|
||||
};
|
||||
export const postSettings = async (data) => axios.post(`${ontimeURL}/settings`, data);
|
||||
|
||||
export const getInfo = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/info`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postInfo = async (data) => {
|
||||
await axios.post(`${ontimeURL}/info`, data);
|
||||
};
|
||||
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
|
||||
|
||||
export const getAliases = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/aliases`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postAliases = async (data) => {
|
||||
await axios.post(`${ontimeURL}/aliases`, data);
|
||||
};
|
||||
|
||||
export const postAliases = async (data) => axios.post(`${ontimeURL}/aliases`, data);
|
||||
|
||||
export const getUserFields = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/userfields`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postUserFields = async (data) => {
|
||||
await axios.post(`${ontimeURL}/userfields`, data);
|
||||
};
|
||||
export const postUserFields = async (data) => axios.post(`${ontimeURL}/userfields`, data);
|
||||
|
||||
export const getOSC = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/osc`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postOSC = async (data) => {
|
||||
await axios.post(`${ontimeURL}/osc`, data);
|
||||
};
|
||||
export const postOSC = async (data) => axios.post(`${ontimeURL}/osc`, data);
|
||||
|
||||
export const downloadEvents = async () => {
|
||||
await axios({
|
||||
@@ -151,13 +140,13 @@ export const downloadEvents = async () => {
|
||||
method: 'GET',
|
||||
responseType: 'blob', // important
|
||||
}).then((response) => {
|
||||
let headerLine = response.headers['Content-Disposition'];
|
||||
const headerLine = response.headers['Content-Disposition'];
|
||||
let filename = 'events.json';
|
||||
|
||||
// try and get the filename from the response
|
||||
if (headerLine != null) {
|
||||
let startFileNameIndex = headerLine.indexOf('"') + 1;
|
||||
let endFileNameIndex = headerLine.lastIndexOf('"');
|
||||
const startFileNameIndex = headerLine.indexOf('"') + 1;
|
||||
const endFileNameIndex = headerLine.lastIndexOf('"');
|
||||
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
||||
}
|
||||
|
||||
@@ -173,14 +162,11 @@ export const downloadEvents = async () => {
|
||||
export const uploadEvents = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file); // appending file
|
||||
await axios
|
||||
.post(`${ontimeURL}/db`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
await axios.post(`${ontimeURL}/db`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadEventsWithPath = async (filepath) => {
|
||||
await axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
};
|
||||
export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
|
||||
@@ -1,37 +1,16 @@
|
||||
import axios from 'axios';
|
||||
import { playbackURL } from '../api/apiConstants';
|
||||
import { playbackURL } from './apiConstants';
|
||||
|
||||
export const getStart = async () => {
|
||||
const res = await axios.get(playbackURL + '/start');
|
||||
return res;
|
||||
};
|
||||
export const getStart = async () => axios.get(`${playbackURL}/start`);
|
||||
|
||||
export const getPause = async () => {
|
||||
const res = axios.get(playbackURL + '/pause');
|
||||
return res;
|
||||
};
|
||||
export const getPause = async () => axios.get(`${playbackURL}/pause`);
|
||||
|
||||
export const getRoll = async () => {
|
||||
const res = axios.get(playbackURL + '/roll');
|
||||
return res;
|
||||
};
|
||||
export const getRoll = async () => axios.get(`${playbackURL}/roll`);
|
||||
|
||||
export const getPrevious = async () => {
|
||||
const res = axios.get(playbackURL + '/previous');
|
||||
return res;
|
||||
};
|
||||
export const getPrevious = async () => axios.get(`${playbackURL}/previous`);
|
||||
|
||||
export const getNext = async () => {
|
||||
const res = axios.get(playbackURL + '/next');
|
||||
return res;
|
||||
};
|
||||
export const getNext = async () => axios.get(`${playbackURL}/next`);
|
||||
|
||||
export const getUnload = async () => {
|
||||
const res = axios.get(playbackURL + '/unload');
|
||||
return res;
|
||||
};
|
||||
export const getUnload = async () => axios.get(`${playbackURL}/unload`);
|
||||
|
||||
export const getReload = async () => {
|
||||
const res = axios.get(playbackURL + '/reload');
|
||||
return res;
|
||||
};
|
||||
export const getReload = async () => axios.get(`${playbackURL}/reload`);
|
||||
|
||||
@@ -31,7 +31,7 @@ export const CursorProvider = ({ children }) => {
|
||||
*/
|
||||
const toggleCursorLocked = useCallback(
|
||||
(newValue = undefined) => {
|
||||
if (newValue === undefined) {
|
||||
if (typeof newValue === 'undefined') {
|
||||
if (isCursorLocked) {
|
||||
cursorLockedOff();
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useSocket } from './socketContext';
|
||||
import { generateId } from 'ontime-utils/generate_id';
|
||||
import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
|
||||
import { generateId } from '../../common/utils/generate_id';
|
||||
import { nowInMillis, stringFromMillis } from '../../common/utils/time';
|
||||
|
||||
export const LoggingContext = createContext({
|
||||
logData: [],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @returns {{catch: string, value: boolean}}
|
||||
*/
|
||||
export const validateTimes = (timeStart, timeEnd) => {
|
||||
let validate = { value: true, catch: '' };
|
||||
const validate = { value: true, catch: '' };
|
||||
if (timeStart > timeEnd) {
|
||||
validate.catch = 'Start time later than end time';
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export default function CollapseBtn(props) {
|
||||
icon={<FiChevronsUp />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ 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;
|
||||
const { clickhandler, active, ref, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Lock cursor to current'>
|
||||
<IconButton
|
||||
@@ -17,6 +17,7 @@ export default function CursorLockedBtn(props) {
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import React, { useState } 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';
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ export default function ExpandBtn(props) {
|
||||
icon={<FiChevronsDown />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
|
||||
@@ -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 PropTypes from 'prop-types';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import style from './CollapseBar.module.scss';
|
||||
|
||||
export default function CollapseBar(props) {
|
||||
const {title = 'Collapse bar', isCollapsed = false, onClick}= props;
|
||||
const { title = 'Collapse bar', isCollapsed, onClick, roll } = props;
|
||||
|
||||
return(
|
||||
<div className={style.header}>
|
||||
return (
|
||||
<div className={roll ? style.headerRoll : style.header}>
|
||||
{title}
|
||||
<Icon
|
||||
className={isCollapsed ? style.moreCollapsed : style.moreExpanded}
|
||||
@@ -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 '../../../styles/main' as *;
|
||||
|
||||
.header,
|
||||
.header__roll {
|
||||
.headerRoll {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: #ccc;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: #ccc;
|
||||
color: $header-gray;
|
||||
}
|
||||
|
||||
.header__roll {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
.headerRoll {
|
||||
color: $ontime-roll;
|
||||
}
|
||||
|
||||
.moreExpanded,
|
||||
.moreCollapsed {
|
||||
cursor: pointer;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.moreExpanded {
|
||||
transform: scaleY(-1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.moreCollapsed {
|
||||
transform: scaleY(1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import React, { memo } from 'react';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Countdown.module.css';
|
||||
import styles from './Countdown.module.scss';
|
||||
|
||||
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
// prepare display string
|
||||
const display =
|
||||
time != null && !isNaN(time)
|
||||
? formatDisplay(time, hideZeroHours)
|
||||
: '-- : -- : --';
|
||||
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
|
||||
|
||||
const colour = isNegative ? '#ff7597' : '#fffffa';
|
||||
const classes = `${small ? styles.countdownClockSmall : styles.countdownClock}
|
||||
${isNegative ? styles.negative : ''}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={small ? styles.countdownClockSmall : styles.countdownClock}
|
||||
style={{ color: colour }}
|
||||
>
|
||||
<div className={classes}>
|
||||
{display}
|
||||
</div>
|
||||
);
|
||||
|
||||
+6
-2
@@ -1,10 +1,11 @@
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap'); */
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
/* Common */
|
||||
.countdownClock,
|
||||
.countdownClockSmall {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
line-height: 0.9;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
/* Viewers */
|
||||
@@ -21,5 +22,8 @@
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 600;
|
||||
color: #fffffa;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import style from './ErrorBoundary.module.scss';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
static contextType = LoggingContext;
|
||||
reportContent = '';
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -21,11 +23,25 @@ class ErrorBoundary extends React.Component {
|
||||
errorInfo: info,
|
||||
});
|
||||
this.context.emitError(error.toString());
|
||||
this.reportContent = `${error} ${info.componentStack}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.errorMessage) {
|
||||
return <p>:/</p>;
|
||||
return (
|
||||
<div className={style.errorContainer}>
|
||||
<div>
|
||||
<p className={style.error}>:/</p>
|
||||
<p>Something went wrong</p>
|
||||
<p
|
||||
className={style.report}
|
||||
onClick={() => navigator.clipboard.writeText(this.reportContent)}
|
||||
>
|
||||
Copy error
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.errorContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: #121212;
|
||||
color: white;
|
||||
|
||||
.error {
|
||||
color: $ontime-pink;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.report {
|
||||
text-decoration: underline $ontime-pink;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.report:hover {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
.report:active {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@@ -1,112 +1,9 @@
|
||||
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 PropTypes from 'prop-types';
|
||||
|
||||
const label = {
|
||||
fontSize: '0.75em',
|
||||
color: '#aaa',
|
||||
};
|
||||
|
||||
const TimesDelayed = (props) => {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span style={label}>
|
||||
Start <span>{scheduledStart}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
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 TimesDelayed from './TimesDelayed';
|
||||
import Times from './Times';
|
||||
|
||||
export default function EventTimesVertical(props) {
|
||||
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
|
||||
@@ -129,11 +26,7 @@ export default function EventTimesVertical(props) {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else if (entry === 'durationOverride') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else return entry === 'durationOverride';
|
||||
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import EditableTimer from '../../input/EditableTimer';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Times.module.scss'
|
||||
|
||||
export default function Times(props) {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>Start</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>End</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 0.75em;
|
||||
color: $label-gray;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import EditableTimer from '../../input/EditableTimer';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
import style from './Times.module.scss'
|
||||
|
||||
export default function TimesDelayed(props) {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>
|
||||
Start <span>{scheduledStart}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { clamp } from 'app/utils/math';
|
||||
import styles from './MyProgressBar.module.css';
|
||||
import styles from './MyProgressBar.module.scss';
|
||||
|
||||
export default function MyProgressBar(props) {
|
||||
const { now, complete, showElapsed } = props;
|
||||
|
||||
+5
-4
@@ -1,22 +1,23 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.progress,
|
||||
.progressCountdown {
|
||||
height: 2vh;
|
||||
background-color: rgba(255, 255, 255, 0.13);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: rgba(255, 255, 255, 0.13);
|
||||
background-color: $bg-gray-900;
|
||||
}
|
||||
|
||||
.progressCountdown {
|
||||
background-color: #ff7597;
|
||||
background-color: $ontime-pink;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
height: 2vh;
|
||||
background-color: rgb(255, 255, 255);
|
||||
background-color: $title-white;
|
||||
border-radius: 4px;
|
||||
transition: 1s linear;
|
||||
transition-property: width;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
@@ -7,7 +7,7 @@ import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import style from './NavLogo.module.scss';
|
||||
|
||||
export default function NavLogo(props) {
|
||||
const {isHidden} = props;
|
||||
const { isHidden } = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -34,74 +34,43 @@ export default function NavLogo(props) {
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
const baseOpacity = (isHidden) ? 0 : 0.5
|
||||
const baseOpacity = isHidden ? 0 : 0.5;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: baseOpacity }}
|
||||
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
className={style.navContainer}
|
||||
>
|
||||
<Image
|
||||
alt=''
|
||||
src={navlogo}
|
||||
className={style.logo}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
|
||||
<AnimatePresence>
|
||||
{showNav && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0, y: -50 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, scaleY: 0, y: -50 }}
|
||||
className={showNav ? style.nav : style.navHidden}
|
||||
className={style.nav}
|
||||
>
|
||||
<Link
|
||||
to='/timer'
|
||||
className={style.navItem}
|
||||
tabIndex={1}
|
||||
>
|
||||
<Link to='/timer' className={style.navItem} tabIndex={1}>
|
||||
Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/minimal'
|
||||
className={style.navItem}
|
||||
tabIndex={2}
|
||||
>
|
||||
<Link to='/minimal' className={style.navItem} tabIndex={2}>
|
||||
Minimal Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/sm'
|
||||
className={style.navItem}
|
||||
tabIndex={3}
|
||||
>
|
||||
<Link to='/sm' className={style.navItem} tabIndex={3}>
|
||||
Backstage
|
||||
</Link>
|
||||
<Link
|
||||
to='/public'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
>
|
||||
<Link to='/public' className={style.navItem} tabIndex={4}>
|
||||
Public
|
||||
</Link>
|
||||
<Link
|
||||
to='/lower'
|
||||
className={style.navItem}
|
||||
tabIndex={5}
|
||||
>
|
||||
<Link to='/lower' className={style.navItem} tabIndex={5}>
|
||||
Lower Thirds
|
||||
</Link>
|
||||
<Link
|
||||
to='/pip'
|
||||
className={style.navItem}
|
||||
tabIndex={6}
|
||||
>
|
||||
<Link to='/pip' className={style.navItem} tabIndex={6}>
|
||||
PIP
|
||||
</Link>
|
||||
<Link
|
||||
to='/studio'
|
||||
className={style.navItem}
|
||||
tabIndex={7}
|
||||
>
|
||||
<Link to='/studio' className={style.navItem} tabIndex={7}>
|
||||
Studio Clock
|
||||
</Link>
|
||||
</motion.div>
|
||||
@@ -113,4 +82,4 @@ export default function NavLogo(props) {
|
||||
|
||||
NavLogo.propTypes = {
|
||||
isHidden: PropTypes.bool,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$nav-color: #fff;
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.navContainer {
|
||||
position: absolute;
|
||||
@@ -9,22 +9,24 @@ $nav-color: #fff;
|
||||
align-items: flex-end;
|
||||
font-size: max(1vw, 16px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav {
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav,
|
||||
.showNav {
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.navItem {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $nav-color;
|
||||
.navItem {
|
||||
background-color: $bg-overlay;
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './ProtectRoute.module.scss';
|
||||
import { PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import { AppContext } from '../../../app/context/AppContext';
|
||||
@@ -13,53 +13,77 @@ 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);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
)}
|
||||
</>
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Space bar
|
||||
if (e.keyCode === 13) {
|
||||
handleValidation();
|
||||
}
|
||||
},
|
||||
[handleValidation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
if (isLocal || auth) {
|
||||
return children;
|
||||
} else {
|
||||
return (
|
||||
<div className={style.container}>
|
||||
ontime
|
||||
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
|
||||
<PinInput
|
||||
type='alphanumeric'
|
||||
size='lg'
|
||||
mask
|
||||
autoFocus
|
||||
value={pin}
|
||||
onChange={(value) => {
|
||||
setFailed(false);
|
||||
setPin(value);
|
||||
}}
|
||||
>
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
</PinInput>
|
||||
<IconButton
|
||||
aria-label='Enter'
|
||||
size='lg'
|
||||
isRound
|
||||
icon={<FiCheck />}
|
||||
style={{ fontSize: '1.5em' }}
|
||||
onClick={() => handleValidation()}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProtectRoute.propTypes = {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@use '../../../styles/mixins' as *;
|
||||
|
||||
.container {
|
||||
background: #222;
|
||||
background: $bg-black;
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
@@ -18,8 +18,6 @@
|
||||
|
||||
.pin,
|
||||
.pin__failed {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
|
||||
input {
|
||||
|
||||
@@ -1,33 +1,53 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import TodayItem from './TodayItem';
|
||||
import style from './Paginator.module.css';
|
||||
import { useInterval } from 'app/hooks/useInterval';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Paginator.module.scss';
|
||||
import Empty from '../../state/Empty';
|
||||
|
||||
export default function Paginator(props) {
|
||||
const { events, selectedId, limit = 7, time = 10, isBackstage } = props;
|
||||
const {
|
||||
events,
|
||||
selectedId,
|
||||
limit = 8,
|
||||
time = 10,
|
||||
isBackstage,
|
||||
setPageNumber,
|
||||
setCurrentPage,
|
||||
} = props;
|
||||
const LIMIT_PER_PAGE = limit;
|
||||
const SCROLL_TIME = time * 1000 || 10000;
|
||||
const SCROLL_TIME = time * 1000;
|
||||
const [numEvents, setNumEvents] = useState(0);
|
||||
const [page, setPage] = useState([]);
|
||||
const [pages, setPages] = useState(0);
|
||||
const [selPage, setSelPage] = useState(0);
|
||||
|
||||
// keep parent up to date
|
||||
useEffect(() => {
|
||||
if (setPageNumber) {
|
||||
setPageNumber(pages);
|
||||
}
|
||||
}, [setPageNumber, pages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setCurrentPage) {
|
||||
setCurrentPage(selPage);
|
||||
}
|
||||
}, [setCurrentPage, selPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (events == null) return;
|
||||
// how many events in list
|
||||
let n = events.length;
|
||||
const n = events.length;
|
||||
setNumEvents(n);
|
||||
|
||||
// how many paginated views
|
||||
let p = Math.ceil(n / LIMIT_PER_PAGE);
|
||||
setPages(p);
|
||||
setPages(Math.ceil(n / LIMIT_PER_PAGE));
|
||||
|
||||
// divide events in parts of LIMIT_PER_PAGE
|
||||
const eventStart = LIMIT_PER_PAGE * selPage;
|
||||
const eventEnd = LIMIT_PER_PAGE * (selPage + 1);
|
||||
let e = events.slice(eventStart, eventEnd);
|
||||
setPage(e);
|
||||
setPage(events.slice(eventStart, eventEnd));
|
||||
|
||||
// if array is completely in past, show depending on SCROLL_PAST
|
||||
}, [events, selPage, LIMIT_PER_PAGE]);
|
||||
@@ -42,14 +62,10 @@ export default function Paginator(props) {
|
||||
|
||||
let selectedState = 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.nav}>
|
||||
{pages > 1 &&
|
||||
[...Array(pages)].map((p, i) => (
|
||||
<div key={i} className={i === selPage ? style.navItemSelected : style.navItem} />
|
||||
))}
|
||||
</div>
|
||||
if (events?.length < 1) {
|
||||
return <Empty text='No events to show' />;
|
||||
} else {
|
||||
return (
|
||||
<div className={style.entries}>
|
||||
{page.map((e) => {
|
||||
if (e.id === selectedId) selectedState = 1;
|
||||
@@ -67,8 +83,8 @@ export default function Paginator(props) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Paginator.propTypes = {
|
||||
@@ -77,4 +93,6 @@ Paginator.propTypes = {
|
||||
limit: PropTypes.number,
|
||||
time: PropTypes.number,
|
||||
isBackstage: PropTypes.bool,
|
||||
setPageNumber: PropTypes.func,
|
||||
setCurrentPage: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
.entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navItem,
|
||||
.navItemSelected {
|
||||
background-color: rgba(255, 255, 255, 0.39);
|
||||
width: 0.7vw;
|
||||
height: 0.7vw;
|
||||
border-radius: 0.35vw;
|
||||
}
|
||||
|
||||
.navItemSelected {
|
||||
background-color: rgba(255, 255, 255, 0.79);
|
||||
}
|
||||
|
||||
.nav {
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-bottom: 2.5vh;
|
||||
}
|
||||
|
||||
.nav > div {
|
||||
margin-left: 0.5vw;
|
||||
}
|
||||
|
||||
.entryTimes {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entryPast {
|
||||
background-color: rgba(255, 255, 255, 0.01);
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
.entryPast > .entryTitle {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.entryFuture {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.entryNow {
|
||||
color: #fff;
|
||||
line-height: 5vh;
|
||||
background-color: rgba(255, 255, 255, 0.07);
|
||||
border-bottom: 0.5vh solid #ff7597aa;
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.backstageInd {
|
||||
color: #ff7597aa;
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
}
|
||||
|
||||
.backstageInd::before {
|
||||
content: '*';
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.entries {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entryTimes {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entryPast {
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
.entryPast > .entryTitle {
|
||||
color: $text-gray-disabled;
|
||||
}
|
||||
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.entryFuture {
|
||||
background-color: $bg-gray-950;
|
||||
}
|
||||
|
||||
.entryNow {
|
||||
line-height: 5vh;
|
||||
background-color: $bg-gray-900;
|
||||
border-bottom: 0.5vh solid $ontime-pink;
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.backstageInd {
|
||||
color: $ontime-pink;
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
}
|
||||
|
||||
.backstageInd::before {
|
||||
content: '*';
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import style from './TitleCard.module.css';
|
||||
import style from './TitleCard.module.scss';
|
||||
|
||||
export default function TitleCard(props) {
|
||||
const { label, title, subtitle, presenter } = props;
|
||||
|
||||
+6
-9
@@ -1,27 +1,24 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
@include card-label;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle,
|
||||
.presenter {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ddd;
|
||||
@include card-title;
|
||||
font-size: 2.5vw;
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
|
||||
}
|
||||
|
||||
.subtitle,
|
||||
.presenter {
|
||||
color: #aaa;
|
||||
color: $subtitle-gray;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import style from './TitleSide.module.css';
|
||||
import style from './TitleSide.module.scss';
|
||||
|
||||
export default function TitleSide(props) {
|
||||
const { type, label, title, subtitle, presenter } = props;
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
@include card-label;
|
||||
}
|
||||
|
||||
.nowTitle,
|
||||
.nextTitle {
|
||||
color: #ddd;
|
||||
font-weight: 600;
|
||||
@include card-title;
|
||||
}
|
||||
|
||||
.nowSubtitle,
|
||||
.nowPresenter,
|
||||
.nextSubtitle,
|
||||
.nextPresenter {
|
||||
color: #aaa;
|
||||
color: $subtitle-gray;
|
||||
}
|
||||
|
||||
.nowTitle {
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import style from './Paginator.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
import style from './Paginator.module.scss';
|
||||
|
||||
export default function TodayItem(props) {
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
|
||||
@@ -19,7 +19,7 @@ export default function TodayItem(props) {
|
||||
else if (selected === 2) selectStyle = style.entryFuture;
|
||||
return (
|
||||
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : undefined}`}>
|
||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : ''}`}>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className={style.entryTitle}>{title}</div>
|
||||
@@ -29,7 +29,7 @@ export default function TodayItem(props) {
|
||||
}
|
||||
|
||||
TodayItem.propTypes = {
|
||||
selected: PropTypes.bool,
|
||||
selected: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
.preview {
|
||||
color: #666;
|
||||
color: $bg-gray-500;
|
||||
}
|
||||
|
||||
.inline {
|
||||
|
||||
@@ -1,35 +1,16 @@
|
||||
import React, { 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 { stringFromMillis } from '../utils/time';
|
||||
import style from './EditableTimer.module.scss';
|
||||
|
||||
export default function EditableTimer(props) {
|
||||
const { name, actionHandler, time, delay, validate, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
// prepare time fields
|
||||
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) => {
|
||||
// Check if there is anything there
|
||||
@@ -67,6 +48,26 @@ export default function EditableTimer(props) {
|
||||
return true;
|
||||
};
|
||||
|
||||
// prepare time fields
|
||||
const validateValue = (value) => {
|
||||
const success = handleSubmit(value);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(value);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [time, delay, emitError]);
|
||||
|
||||
const isDelayed = delay != null && delay !== 0;
|
||||
|
||||
return (
|
||||
|
||||
+5
-3
@@ -1,7 +1,9 @@
|
||||
@use '../../styles/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,13 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
||||
import style from './Empty.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Empty.module.scss';
|
||||
|
||||
export default function Empty(props) {
|
||||
const { text } = props;
|
||||
const { text, ...rest } = props;
|
||||
return (
|
||||
<div className={style.emptyContainer}>
|
||||
<div className={style.emptyContainer} {...rest}>
|
||||
<Emptyimage className={style.empty} />
|
||||
<span className={style.text}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Empty.propTypes = {
|
||||
text: PropTypes.string,
|
||||
}
|
||||
+3
-3
@@ -1,3 +1,5 @@
|
||||
@use '../../styles/main' as *;
|
||||
|
||||
.emptyContainer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
@@ -5,13 +7,11 @@
|
||||
|
||||
.empty {
|
||||
width: 100%;
|
||||
margin: auto 0;
|
||||
margin-top: 10vh;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: rgba(0, 0, 0, 0.17);
|
||||
color: $bg-black-300;
|
||||
font-weight: 600;
|
||||
font-size: 2em;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
millisToSeconds,
|
||||
timeStringToMillis,
|
||||
} from '../dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { stringFromMillis } from '../time';
|
||||
|
||||
describe('test string from formatDisplay function', () => {
|
||||
it('test with null values', () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function formatDisplay(seconds, hideZero = false) {
|
||||
const minutes = Math.floor((s % 3600) / 60);
|
||||
|
||||
if (hideZero && hours < 1) return [minutes, s % 60].map(format).join(':');
|
||||
else return [hours, minutes, s % 60].map(format).join(':');
|
||||
return [hours, minutes, s % 60].map(format).join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ export const timeStringToMillis = (string) => {
|
||||
if (time.length === 1) return Math.abs(time[0] * mts);
|
||||
if (time.length === 2) return Math.abs(time[0]) * mtm + time[1] * mts;
|
||||
if (time.length === 3) return Math.abs(time[0]) * mth + time[1] * mtm + time[2] * mts;
|
||||
else return 0;
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { stringFromMillis } from './time';
|
||||
|
||||
/**
|
||||
* @description From a list of events, returns only events of type event with calculated delays
|
||||
* @param {Object[]} events - given events
|
||||
* @returns {Object[]} Filtered events with calculated delays
|
||||
*/
|
||||
|
||||
export const getEventsWithDelay = (events) => {
|
||||
if (events == null) return [];
|
||||
|
||||
@@ -66,7 +67,7 @@ export const formatEventList = (events, selectedId, nextId, showEnd = false) =>
|
||||
const givenEvents = [...events];
|
||||
|
||||
// format list
|
||||
let formattedEvents = [];
|
||||
const formattedEvents = [];
|
||||
for (const g of givenEvents) {
|
||||
const start = stringFromMillis(g.timeStart, false);
|
||||
const end = stringFromMillis(g.timeEnd, false);
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import StartIconBtn from 'common/components/buttons/StartIconBtn';
|
||||
import PauseIconBtn from 'common/components/buttons/PauseIconBtn';
|
||||
import PrevIconBtn from 'common/components/buttons/PrevIconBtn';
|
||||
import NextIconBtn from 'common/components/buttons/NextIconBtn';
|
||||
import RollIconBtn from 'common/components/buttons/RollIconBtn';
|
||||
import UnloadIconBtn from 'common/components/buttons/UnloadIconBtn';
|
||||
import ReloadIconButton from 'common/components/buttons/ReloadIconBtn';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.playback === nextProps.playback
|
||||
&& prevProps.selectedId === nextProps.selectedId
|
||||
&& prevProps.noEvents === nextProps.noEvents
|
||||
);
|
||||
};
|
||||
|
||||
const Playback = (props) => {
|
||||
const { playback, selectedId, playbackControl, noEvents } = props;
|
||||
const isRolling = playback === 'roll';
|
||||
|
||||
return (
|
||||
<div className={style.playbackContainer}>
|
||||
<StartIconBtn
|
||||
active={playback === 'start'}
|
||||
clickhandler={() => playbackControl('start')}
|
||||
disabled={!selectedId || isRolling || noEvents}
|
||||
/>
|
||||
<PauseIconBtn
|
||||
active={playback === 'pause'}
|
||||
clickhandler={() => playbackControl('pause')}
|
||||
disabled={!selectedId || isRolling || noEvents || playback !== 'start'}
|
||||
/>
|
||||
<RollIconBtn
|
||||
active={playback === 'roll'}
|
||||
disabled={playback === 'roll' || noEvents}
|
||||
clickhandler={() => playbackControl('roll')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Transport = (props) => {
|
||||
const { playback, selectedId, playbackControl, noEvents } = props;
|
||||
const isRolling = playback === 'roll';
|
||||
|
||||
return (
|
||||
<div className={style.playbackContainer}>
|
||||
<PrevIconBtn
|
||||
clickhandler={() => playbackControl('previous')}
|
||||
disabled={isRolling || noEvents}
|
||||
/>
|
||||
<NextIconBtn
|
||||
clickhandler={() => playbackControl('next')}
|
||||
disabled={isRolling || noEvents}
|
||||
/>
|
||||
<ReloadIconButton
|
||||
clickhandler={() => playbackControl('reload')}
|
||||
disabled={selectedId == null || isRolling || noEvents}
|
||||
/>
|
||||
<UnloadIconBtn
|
||||
clickhandler={() => playbackControl('unload')}
|
||||
disabled={(selectedId == null && !isRolling) || noEvents}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PlaybackButtons = (props) => {
|
||||
const { playback, selectedId, noEvents, playbackControl } = props;
|
||||
return (
|
||||
<>
|
||||
<Playback
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
noEvents={noEvents}
|
||||
playbackControl={playbackControl}
|
||||
/>
|
||||
<Transport
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
noEvents={noEvents}
|
||||
playbackControl={playbackControl}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PlaybackButtons, areEqual);
|
||||
|
||||
PlaybackButtons.propTypes = {
|
||||
playback: PropTypes.string,
|
||||
selectedId: PropTypes.string,
|
||||
playbackControl: PropTypes.func.isRequired,
|
||||
noEvents: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
Transport.propTypes = {
|
||||
playback: PropTypes.string,
|
||||
selectedId: PropTypes.string,
|
||||
playbackControl: PropTypes.func.isRequired,
|
||||
noEvents: PropTypes.bool.isRequired,
|
||||
};
|
||||
@@ -1,140 +0,0 @@
|
||||
import React from 'react';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import Countdown from 'common/components/countdown/Countdown';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.timer.running === nextProps.timer.running &&
|
||||
prevProps.timer.isNegative === nextProps.timer.isNegative &&
|
||||
prevProps.timer.expectedFinish === nextProps.timer.expectedFinish &&
|
||||
prevProps.timer.startedAt === nextProps.timer.startedAt &&
|
||||
prevProps.playback === nextProps.playback &&
|
||||
prevProps.timer.secondary === nextProps.timer.secondary &&
|
||||
prevProps.selectedId === nextProps.selectedId
|
||||
);
|
||||
};
|
||||
|
||||
const incrementProps = {
|
||||
size: 'sm',
|
||||
width: '2.9em',
|
||||
colorScheme: 'whiteAlpha',
|
||||
variant: 'outline',
|
||||
_focus: { boxShadow: 'none' },
|
||||
};
|
||||
|
||||
const PlaybackTimer = (props) => {
|
||||
const { timer, playback, handleIncrement, selectedId } = props;
|
||||
const started = stringFromMillis(timer.startedAt, true);
|
||||
const finish = stringFromMillis(timer.expectedFinish, true);
|
||||
const isRolling = playback === 'roll';
|
||||
const isWaiting = timer.secondary > 0 && timer.running == null;
|
||||
const disableButtons = selectedId == null || isRolling;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.timeContainer}>
|
||||
<div className={style.indicators}>
|
||||
<Tooltip label='Roll mode active'>
|
||||
<div className={isRolling ? style.indRollActive : style.indRoll} />
|
||||
</Tooltip>
|
||||
<div
|
||||
className={timer.isNegative ? style.indNegativeActive : style.indNegative}
|
||||
/>
|
||||
<div className={style.indDelay} />
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<Countdown
|
||||
time={isWaiting ? timer.secondary : timer.running}
|
||||
isNegative={timer.isNegative}
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
{isWaiting ? (
|
||||
<div className={style.roll}>
|
||||
<span className={style.rolltag}>Roll: Countdown to start</span>
|
||||
<span className={style.time}>FIX</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={style.start}>
|
||||
<span className={style.tag}>Started at </span>
|
||||
<span className={style.time}>{started}</span>
|
||||
</div>
|
||||
<div className={style.finish}>
|
||||
<span className={style.tag}>Finish at </span>
|
||||
<span className={style.time}>{finish}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={style.btn}>
|
||||
<Tooltip
|
||||
label='Remove 1 minute'
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(-1)}
|
||||
>
|
||||
-1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label='Add 1 minute'
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(1)}
|
||||
>
|
||||
+1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label='Remove 5 minutes'
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(-5)}
|
||||
>
|
||||
-5
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label='Add 5 minutes'
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
onClick={() => handleIncrement(5)}
|
||||
>
|
||||
+5
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PlaybackTimer, areEqual);
|
||||
|
||||
PlaybackTimer.propTypes = {
|
||||
timer: PropTypes.object.isRequired,
|
||||
playback: PropTypes.string,
|
||||
handleIncrement: PropTypes.func.isRequired,
|
||||
selectedId: PropTypes.string,
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import SocketProvider from 'app/context/socketContext';
|
||||
import MessageControl from '../MessageControl';
|
||||
import MessageControl from '../message/MessageControl';
|
||||
|
||||
// need to inject the socket provider to make component
|
||||
// render without failing
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import SocketProvider from 'app/context/socketContext';
|
||||
import PlaybackControl from '../PlaybackControl';
|
||||
import PlaybackControl from '../playback/PlaybackControl';
|
||||
|
||||
test('check that playback control renders', async () => {
|
||||
// need to inject the socket provider to make component
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import VisibleIconBtn from '../../../common/components/buttons/VisibleIconBtn';
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
const inputProps = {
|
||||
size: 'sm',
|
||||
};
|
||||
|
||||
export default function InputRow(props) {
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>{label}</span>
|
||||
<div className={style.inputItems}>
|
||||
<Editable
|
||||
onChange={(event) => changeHandler(event)}
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
className={style.inline}
|
||||
color={text === '' ? '#666' : 'inherit'}
|
||||
>
|
||||
<EditablePreview className={style.padleft} />
|
||||
<EditableInput className={style.padleft} />
|
||||
</Editable>
|
||||
<VisibleIconBtn
|
||||
active={visible || undefined}
|
||||
actionHandler={actionHandler}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+2
-34
@@ -1,41 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn';
|
||||
import OnAirIconBtn from '../../common/components/buttons/OnAirIconBtn';
|
||||
import InputRow from './InputRow';
|
||||
import OnAirIconBtn from '../../../common/components/buttons/OnAirIconBtn';
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
const inputProps = {
|
||||
size: 'sm',
|
||||
};
|
||||
|
||||
const InputRow = (props) => {
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>{label}</span>
|
||||
<div className={style.inputItems}>
|
||||
<Editable
|
||||
onChange={(event) => changeHandler(event)}
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
className={style.inline}
|
||||
color={text === '' ? '#666' : 'inherit'}
|
||||
>
|
||||
<EditablePreview className={style.padleft} />
|
||||
<EditableInput className={style.padleft} />
|
||||
</Editable>
|
||||
<VisibleIconBtn
|
||||
active={visible || undefined}
|
||||
actionHandler={actionHandler}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function MessageControl() {
|
||||
const socket = useSocket();
|
||||
const [pres, setPres] = useState({
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
@use '../../styles/main' as *;
|
||||
@use '../../styles/mixins' as *;
|
||||
@use '../../../styles/main' as *;
|
||||
@use '../../../styles/mixins' as *;
|
||||
|
||||
.messageContainer,
|
||||
.onAirToggle {
|
||||
@@ -22,13 +22,13 @@
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: #ccc;
|
||||
color: $label-gray;
|
||||
}
|
||||
|
||||
.inline {
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background-color: $input-bg;
|
||||
border: $input-border;
|
||||
}
|
||||
|
||||
.padleft {
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import StartIconBtn from '../../../common/components/buttons/StartIconBtn';
|
||||
import PauseIconBtn from '../../../common/components/buttons/PauseIconBtn';
|
||||
import RollIconBtn from '../../../common/components/buttons/RollIconBtn';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
|
||||
export default function Playback(props) {
|
||||
const { playback, selectedId, playbackControl, noEvents } = props;
|
||||
const isRolling = playback === 'roll';
|
||||
|
||||
return (
|
||||
<div className={style.playbackContainer}>
|
||||
<StartIconBtn
|
||||
active={playback === 'start'}
|
||||
clickhandler={() => playbackControl('start')}
|
||||
disabled={!selectedId || isRolling || noEvents}
|
||||
/>
|
||||
<PauseIconBtn
|
||||
active={playback === 'pause'}
|
||||
clickhandler={() => playbackControl('pause')}
|
||||
disabled={!selectedId || isRolling || noEvents || playback !== 'start'}
|
||||
/>
|
||||
<RollIconBtn
|
||||
active={playback === 'roll'}
|
||||
disabled={playback === 'roll' || noEvents}
|
||||
clickhandler={() => playbackControl('roll')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Playback.propTypes = {
|
||||
playback: PropTypes.string,
|
||||
selectedId: PropTypes.string,
|
||||
playbackControl: PropTypes.func.isRequired,
|
||||
noEvents: PropTypes.bool.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Transport from './Transport';
|
||||
import Playback from './Playback';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.playback === nextProps.playback &&
|
||||
prevProps.selectedId === nextProps.selectedId &&
|
||||
prevProps.noEvents === nextProps.noEvents
|
||||
);
|
||||
};
|
||||
|
||||
const PlaybackButtons = (props) => {
|
||||
const { playback, selectedId, noEvents, playbackControl } = props;
|
||||
return (
|
||||
<>
|
||||
<Playback
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
noEvents={noEvents}
|
||||
playbackControl={playbackControl}
|
||||
/>
|
||||
<Transport
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
noEvents={noEvents}
|
||||
playbackControl={playbackControl}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PlaybackButtons, areEqual);
|
||||
|
||||
PlaybackButtons.propTypes = {
|
||||
playback: PropTypes.string,
|
||||
selectedId: PropTypes.string,
|
||||
playbackControl: PropTypes.func.isRequired,
|
||||
noEvents: PropTypes.bool.isRequired,
|
||||
};
|
||||
+1
-1
@@ -63,7 +63,7 @@ export default function PlaybackControl() {
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const playbackControl = async (action, payload) => {
|
||||
const playbackControl = async (action) => {
|
||||
switch (action) {
|
||||
case 'start':
|
||||
socket.emit('set-playstate', 'start');
|
||||
+12
-12
@@ -1,16 +1,16 @@
|
||||
@use '../../../styles/main' as *;
|
||||
@use '../../../styles/mixins' as *;
|
||||
|
||||
.mainContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
display: grid;
|
||||
margin: 0 auto;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.timeContainer,
|
||||
.playbackContainer {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
@include second-container;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
.indRoll,
|
||||
.indDelay,
|
||||
.indNegative {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
background-color: $bg-black-300;
|
||||
}
|
||||
|
||||
.indRoll,
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
|
||||
.indRollActive {
|
||||
background-color: #2b6cb0;
|
||||
background-color: $ontime-roll;
|
||||
}
|
||||
|
||||
.indNegative,
|
||||
@@ -67,11 +67,11 @@
|
||||
}
|
||||
|
||||
.indNegativeActive {
|
||||
background-color: #ff7597;
|
||||
background-color: $ontime-pink;
|
||||
}
|
||||
|
||||
.indDelayActive {
|
||||
background-color: #dd6b20;
|
||||
background-color: $ontime-delay;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@@ -102,17 +102,17 @@
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #ccc;
|
||||
color: $header-gray;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: #aaa;
|
||||
color: $label-gray;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.rolltag {
|
||||
color: #2b6cb0;
|
||||
color: $ontime-roll;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { memo } from 'react';
|
||||
import Countdown from 'common/components/countdown/Countdown';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringFromMillis } from '../../../common/utils/time';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.timer.running === nextProps.timer.running &&
|
||||
prevProps.timer.isNegative === nextProps.timer.isNegative &&
|
||||
prevProps.timer.expectedFinish === nextProps.timer.expectedFinish &&
|
||||
prevProps.timer.startedAt === nextProps.timer.startedAt &&
|
||||
prevProps.playback === nextProps.playback &&
|
||||
prevProps.timer.secondary === nextProps.timer.secondary &&
|
||||
prevProps.selectedId === nextProps.selectedId
|
||||
);
|
||||
};
|
||||
|
||||
const incrementProps = {
|
||||
size: 'sm',
|
||||
width: '2.9em',
|
||||
colorScheme: 'whiteAlpha',
|
||||
variant: 'outline',
|
||||
_focus: { boxShadow: 'none' },
|
||||
};
|
||||
|
||||
const PlaybackTimer = (props) => {
|
||||
const { timer, playback, handleIncrement, selectedId } = props;
|
||||
const started = stringFromMillis(timer.startedAt, true);
|
||||
const finish = stringFromMillis(timer.expectedFinish, true);
|
||||
const isRolling = playback === 'roll';
|
||||
const isWaiting = timer.secondary > 0 && timer.running == null;
|
||||
const disableButtons = selectedId == null || isRolling;
|
||||
|
||||
return (
|
||||
<div className={style.timeContainer}>
|
||||
<div className={style.indicators}>
|
||||
<Tooltip label='Roll mode active'>
|
||||
<div className={isRolling ? style.indRollActive : style.indRoll} />
|
||||
</Tooltip>
|
||||
<div className={timer.isNegative ? style.indNegativeActive : style.indNegative} />
|
||||
<div className={style.indDelay} />
|
||||
</div>
|
||||
<div className={style.timer}>
|
||||
<Countdown
|
||||
time={isWaiting ? timer.secondary : timer.running}
|
||||
isNegative={timer.isNegative}
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
{isWaiting ? (
|
||||
<div className={style.roll}>
|
||||
<span className={style.rolltag}>Roll: Countdown to start</span>
|
||||
<span className={style.time}>FIX</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={style.start}>
|
||||
<span className={style.tag}>Started at </span>
|
||||
<span className={style.time}>{started}</span>
|
||||
</div>
|
||||
<div className={style.finish}>
|
||||
<span className={style.tag}>Finish at </span>
|
||||
<span className={style.time}>{finish}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={style.btn}>
|
||||
<Tooltip label='Remove 1 minute' delay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(-1)}>
|
||||
-1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 1 minute' delay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(1)}>
|
||||
+1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label='Remove 5 minutes' delay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(-5)}>
|
||||
-5
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 5 minutes' delay={500} shouldWrapChildren={disableButtons}>
|
||||
<Button {...incrementProps} disabled={disableButtons} onClick={() => handleIncrement(5)}>
|
||||
+5
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PlaybackTimer, areEqual);
|
||||
|
||||
PlaybackTimer.propTypes = {
|
||||
timer: PropTypes.object.isRequired,
|
||||
playback: PropTypes.string,
|
||||
handleIncrement: PropTypes.func.isRequired,
|
||||
selectedId: PropTypes.string,
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PrevIconBtn from '../../../common/components/buttons/PrevIconBtn';
|
||||
import NextIconBtn from '../../../common/components/buttons/NextIconBtn';
|
||||
import ReloadIconButton from '../../../common/components/buttons/ReloadIconBtn';
|
||||
import UnloadIconBtn from '../../../common/components/buttons/UnloadIconBtn';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
|
||||
export default function Transport(props) {
|
||||
const { playback, selectedId, playbackControl, noEvents } = props;
|
||||
const isRolling = playback === 'roll';
|
||||
|
||||
return (
|
||||
<div className={style.playbackContainer}>
|
||||
<PrevIconBtn
|
||||
clickhandler={() => playbackControl('previous')}
|
||||
disabled={isRolling || noEvents}
|
||||
/>
|
||||
<NextIconBtn
|
||||
clickhandler={() => playbackControl('next')}
|
||||
disabled={isRolling || noEvents}
|
||||
/>
|
||||
<ReloadIconButton
|
||||
clickhandler={() => playbackControl('reload')}
|
||||
disabled={selectedId == null || isRolling || noEvents}
|
||||
/>
|
||||
<UnloadIconBtn
|
||||
clickhandler={() => playbackControl('unload')}
|
||||
disabled={(selectedId == null && !isRolling) || noEvents}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Transport.propTypes = {
|
||||
playback: PropTypes.string,
|
||||
selectedId: PropTypes.string,
|
||||
playbackControl: PropTypes.func.isRequired,
|
||||
noEvents: PropTypes.bool.isRequired,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
@@ -20,10 +21,10 @@ export default function BlockBlock(props) {
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
<div className={style.actionOverlay}>
|
||||
<HStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<DeleteIconBtn actionHandler={actionHandler} />
|
||||
<ActionButtons showAdd showDelay actionHandler={actionHandler} />
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
align-content: center;
|
||||
align-self: flex-start;
|
||||
padding-top: 0.2em;
|
||||
gap: 0.5em;
|
||||
opacity: 0.8;
|
||||
transition: linear 0.1s;
|
||||
justify-self: end;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
@@ -16,7 +17,7 @@ export default function DelayBlock(props) {
|
||||
eventsHandler('applyDelay', { id: data.id, duration: data.duration });
|
||||
};
|
||||
|
||||
let delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
|
||||
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
|
||||
return (
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
@@ -25,11 +26,11 @@ export default function DelayBlock(props) {
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
<DelayInput className={style.input} value={delayValue} actionHandler={actionHandler} />
|
||||
<div className={style.actionOverlay}>
|
||||
<HStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<ApplyIconBtn clickhandler={applyDelayHandler} />
|
||||
<DeleteIconBtn actionHandler={actionHandler} />
|
||||
<ActionButtons showAdd actionHandler={actionHandler} />
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
align-content: center;
|
||||
align-self: flex-start;
|
||||
padding-top: 0.2em;
|
||||
gap: 0.5em;
|
||||
opacity: 0.8;
|
||||
transition: linear 0.1s;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import { CollapseProvider } from '../../app/context/CollapseContext';
|
||||
import styles from './Editor.module.scss';
|
||||
|
||||
const EventListWrapper = lazy(() => import('features/editors/list/EventListWrapper'));
|
||||
const PlaybackControl = lazy(() => import('features/control/PlaybackControl'));
|
||||
const MessageControl = lazy(() => import('features/control/MessageControl'));
|
||||
const PlaybackControl = lazy(() => import('features/control/playback/PlaybackControl'));
|
||||
const MessageControl = lazy(() => import('features/control/message/MessageControl'));
|
||||
const Info = lazy(() => import('features/info/Info'));
|
||||
|
||||
export default function Editor() {
|
||||
@@ -35,7 +35,7 @@ export default function Editor() {
|
||||
<CollapseProvider>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar onOpen={onOpen} isOpen={isOpen} />
|
||||
<MenuBar onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
@use '../../styles/main' as *;
|
||||
|
||||
.mainContainer {
|
||||
background: linear-gradient(90deg, #202020 0%, #121212 100%);
|
||||
background: $bg-black;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
color: #fffd;
|
||||
color: $title-white;
|
||||
padding: max(16px, 2vh);
|
||||
|
||||
display: grid;
|
||||
@@ -94,14 +96,14 @@
|
||||
|
||||
h1 {
|
||||
font-size: max(1.5em, 16px);
|
||||
color: rgba(255, 255, 255, 0.63);
|
||||
color: $bg-gray-100;
|
||||
padding-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.mainContainer > div {
|
||||
border-radius: 0.5em;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.13);
|
||||
background-color: $bg-gray-1000;
|
||||
padding: 0.8em 1.5em;
|
||||
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import EventTimes from '../../../common/components/eventTimes/EventTimes';
|
||||
import EditableText from '../../../common/input/EditableText';
|
||||
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
export default function CollapsedBlock (props) {
|
||||
const { provided, data, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
|
||||
</div>
|
||||
<EventTimes
|
||||
actionHandler={actionHandler}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
className={style.time}
|
||||
/>
|
||||
<div className={style.titleContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
|
||||
/>
|
||||
</div>
|
||||
<HStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
|
||||
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
|
||||
</HStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CollapsedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.any,
|
||||
delayValue: PropTypes.string,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -1,143 +1,13 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import Icon from '@chakra-ui/icon';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import EventTimes from 'common/components/eventTimes/EventTimes';
|
||||
import EventTimesVertical from 'common/components/eventTimes/EventTimesVertical';
|
||||
import EditableText from 'common/input/EditableText';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import PublicIconBtn from 'common/components/buttons/PublicIconBtn';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CollapseContext } from '../../../app/context/CollapseContext';
|
||||
import style from './EventBlock.module.css';
|
||||
|
||||
const ExpandedBlock = (props) => {
|
||||
const { provided, data, eventIndex, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
const oscid = data?.id || '...';
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
|
||||
</div>
|
||||
<div className={style.timeExpanded}>
|
||||
<EventTimesVertical
|
||||
actionHandler={actionHandler}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
duration={data.duration}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
className={style.time}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style.titleContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Presenter'
|
||||
defaultValue={data.presenter}
|
||||
placeholder='Add Presenter name'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'presenter', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Subtitle'
|
||||
defaultValue={data.subtitle}
|
||||
placeholder='Add Subtitle'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'subtitle', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Note'
|
||||
defaultValue={data.note}
|
||||
placeholder='Add Note'
|
||||
style={{ color: '#d69e2e' }}
|
||||
maxchar={160}
|
||||
submitHandler={(v) => actionHandler('update', { field: 'note', value: v })}
|
||||
/>
|
||||
<span className={style.oscLabel}>
|
||||
{`/ontime/goto ${eventIndex + 1} << OSC >> /ontime/gotoid ${oscid}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.actionOverlay}>
|
||||
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
|
||||
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
|
||||
<DeleteIconBtn actionHandler={actionHandler} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ExpandedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
eventIndex: PropTypes.number.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.number,
|
||||
delayValue: PropTypes.string,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const CollapsedBlock = (props) => {
|
||||
const { provided, data, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
|
||||
</div>
|
||||
<EventTimes
|
||||
actionHandler={actionHandler}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
className={style.time}
|
||||
/>
|
||||
<div className={style.titleContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.actionOverlay}>
|
||||
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
|
||||
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CollapsedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.any,
|
||||
delayValue: PropTypes.string,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
import CollapsedBlock from './CollapsedBlock';
|
||||
import ExpandedBlock from './ExpandedBlock';
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
export default function EventBlock(props) {
|
||||
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler, next } = props;
|
||||
|
||||
+4
-3
@@ -1,3 +1,5 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
/* ============= COMMON ============= */
|
||||
.event {
|
||||
margin: 0.2em 0;
|
||||
@@ -8,11 +10,11 @@
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background-color: $bg-gray-950;
|
||||
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.active {
|
||||
@@ -69,7 +71,7 @@
|
||||
.nextDisabled {
|
||||
width: max-content;
|
||||
padding: 0 0.2em;
|
||||
color: #4bffab;
|
||||
color: $ontime-accent;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
@@ -199,7 +201,6 @@
|
||||
align-content: center;
|
||||
align-self: flex-start;
|
||||
padding-top: 0.2em;
|
||||
gap: 0.5em;
|
||||
opacity: 0.8;
|
||||
transition: linear 0.1s;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import EventTimesVertical from '../../../common/components/eventTimes/EventTimesVertical';
|
||||
import EditableText from '../../../common/input/EditableText';
|
||||
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import DeleteIconBtn from '../../../common/components/buttons/DeleteIconBtn';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
export default function ExpandedBlock(props) {
|
||||
const { provided, data, eventIndex, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
const oscid = data?.id || '...';
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
|
||||
</div>
|
||||
<div className={style.timeExpanded}>
|
||||
<EventTimesVertical
|
||||
actionHandler={actionHandler}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
duration={data.duration}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
className={style.time}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style.titleContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Presenter'
|
||||
defaultValue={data.presenter}
|
||||
placeholder='Add Presenter name'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'presenter', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Subtitle'
|
||||
defaultValue={data.subtitle}
|
||||
placeholder='Add Subtitle'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'subtitle', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Note'
|
||||
defaultValue={data.note}
|
||||
placeholder='Add Note'
|
||||
style={{ color: '#d69e2e' }}
|
||||
maxchar={160}
|
||||
submitHandler={(v) => actionHandler('update', { field: 'note', value: v })}
|
||||
/>
|
||||
<span className={style.oscLabel}>
|
||||
{`/ontime/goto ${eventIndex + 1} << OSC >> /ontime/gotoid ${oscid}`}
|
||||
</span>
|
||||
</div>
|
||||
<VStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
|
||||
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
|
||||
<DeleteIconBtn actionHandler={actionHandler} />
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ExpandedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
eventIndex: PropTypes.number.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.number,
|
||||
delayValue: PropTypes.string,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -127,7 +127,7 @@ export default function EventList(props) {
|
||||
}, [selectedId, isCursorLocked]);
|
||||
|
||||
if (events.length < 1) {
|
||||
return <Empty text='No Events' />;
|
||||
return <Empty text='No Events' style={{marginTop: "10vh"}} />;
|
||||
}
|
||||
|
||||
// DND
|
||||
@@ -178,7 +178,7 @@ export default function EventList(props) {
|
||||
)}
|
||||
<div
|
||||
ref={cursor === index ? cursorRef : undefined}
|
||||
className={cursor === index ? style.cursor : undefined}
|
||||
className={cursor === index ? style.cursor : ''}
|
||||
>
|
||||
<EventListItem
|
||||
type={e.type}
|
||||
|
||||
@@ -27,7 +27,6 @@ const EventListItem = (props) => {
|
||||
eventsHandler,
|
||||
delay,
|
||||
previousEnd,
|
||||
...rest
|
||||
} = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
|
||||
@@ -42,6 +41,7 @@ const EventListItem = (props) => {
|
||||
(start, end) => (start > end ? end + 86400000 - start : end - start),
|
||||
[]
|
||||
);
|
||||
|
||||
// Create / delete new events
|
||||
const actionHandler = useCallback(
|
||||
(action, payload) => {
|
||||
@@ -101,7 +101,7 @@ const EventListItem = (props) => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[data, defaultPublic, emitError, eventsHandler, index, starTimeIsLastEnd]
|
||||
[calculateDuration, data, defaultPublic, emitError, eventsHandler, index, starTimeIsLastEnd]
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function EventListWrapper() {
|
||||
}
|
||||
|
||||
// optimistically update object, temp ID until refetch
|
||||
let optimistic = [...previousEvents];
|
||||
const optimistic = [...previousEvents];
|
||||
optimistic.splice(newEvent.order, 0, {
|
||||
...newEvent,
|
||||
id: new Date().toISOString(),
|
||||
@@ -128,8 +128,7 @@ export default function EventListWrapper() {
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
||||
|
||||
let filtered = [...previousEvents];
|
||||
filtered.filter((e) => e.id === 'eventId');
|
||||
const filtered = [...previousEvents].filter((e) => e.id === 'eventId')
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData(EVENTS_TABLE, filtered);
|
||||
@@ -158,7 +157,7 @@ export default function EventListWrapper() {
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
||||
|
||||
let clear = [];
|
||||
const clear = [];
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData(EVENTS_TABLE, clear);
|
||||
@@ -225,13 +224,13 @@ export default function EventListWrapper() {
|
||||
|
||||
// Events API
|
||||
const eventsHandler = useCallback(
|
||||
async (action, payload, options = undefined) => {
|
||||
async (action, payload, options) => {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
try {
|
||||
let newEvent = { ...payload };
|
||||
const newEvent = { ...payload };
|
||||
// there is an option to pass an index of an array to use as start time
|
||||
if (options?.startIsLastEnd !== undefined) {
|
||||
if (typeof options?.startIsLastEnd !== 'undefined') {
|
||||
newEvent.timeStart = data[options.startIsLastEnd].timeEnd || 0;
|
||||
}
|
||||
// hard coding duration value to be as expected for now
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@use '../../../styles/main' as *;
|
||||
@use '../../../styles/mixins' as *;
|
||||
|
||||
.eventContainer {
|
||||
@include second-container;
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
@@ -21,14 +22,6 @@
|
||||
}
|
||||
|
||||
.cursor {
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
#ff7597 2%,
|
||||
#0001 3%,
|
||||
#0001 97%,
|
||||
#ff7597 98%
|
||||
);
|
||||
|
||||
box-shadow: 2px 2px 0 $ontime-pink;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
|
||||
@mixin container {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
@include second-container;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@@ -73,7 +69,7 @@
|
||||
|
||||
.emptyLabel {
|
||||
font-size: 0.8em;
|
||||
color: #555;
|
||||
color: $bg-gray-700;
|
||||
}
|
||||
|
||||
.notes {
|
||||
@@ -85,26 +81,12 @@
|
||||
.if {
|
||||
font-size: 0.8em;
|
||||
color: $ontime-accent;
|
||||
@include container-bg;
|
||||
background-color: $bg-gray-900;
|
||||
padding: 0 0.5em;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
ul > li {
|
||||
font-size: 0.9em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.moreExpanded,
|
||||
.moreCollapsed {
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.moreExpanded {
|
||||
transform: scaleY(-1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.moreCollapsed {
|
||||
transform: scaleY(1);
|
||||
transition: transform 0.3s;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import style from './InfoLogger.module.scss';
|
||||
import CollapseBar from "../../common/components/collapseBar/CollapseBar";
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import style from './InfoLogger.module.scss';
|
||||
|
||||
export default function InfoLogger() {
|
||||
const { logData, clearLog } = useContext(LoggingContext);
|
||||
@@ -16,6 +17,10 @@ export default function InfoLogger() {
|
||||
const [showUser, setShowUser] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchers = [];
|
||||
if (showUser) {
|
||||
matchers.push('USER');
|
||||
@@ -36,12 +41,10 @@ export default function InfoLogger() {
|
||||
matchers.push('PLAYBACK');
|
||||
}
|
||||
|
||||
const d = logData.filter((d) => (
|
||||
matchers.some((m) => d.origin === m)
|
||||
))
|
||||
const d = logData.filter((d) => matchers.some((m) => d.origin === m));
|
||||
|
||||
setData(d);
|
||||
},[logData, showUser, showClient, showServer, showPlayback, showRx, showTx])
|
||||
}, [logData, showUser, showClient, showServer, showPlayback, showRx, showTx]);
|
||||
|
||||
const disableOthers = (toEnable) => {
|
||||
toEnable === 'USER' ? setShowUser(true) : setShowUser(false);
|
||||
@@ -50,62 +53,75 @@ export default function InfoLogger() {
|
||||
toEnable === 'RX' ? setShowRx(true) : setShowRx(false);
|
||||
toEnable === 'TX' ? setShowTx(true) : setShowTx(false);
|
||||
toEnable === 'PLAYBACK' ? setShowPlayback(true) : setShowPlayback(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={collapsed ? style.container : style.container__expanded}>
|
||||
<CollapseBar title='Log' isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)} />
|
||||
{!collapsed && (
|
||||
<>
|
||||
<div className={style.toggleBar}>
|
||||
<HStack className={style.toggleBar}>
|
||||
<div
|
||||
onClick={() => setShowUser((s) => !s)}
|
||||
onAuxClick={() => disableOthers('USER')}
|
||||
className={(showUser) ? style.active : null}>
|
||||
className={showUser ? style.active : null}
|
||||
>
|
||||
USER
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowClient((s) => !s)}
|
||||
onAuxClick={() => disableOthers('CLIENT')}
|
||||
className={(showClient) ? style.active : null}>
|
||||
className={showClient ? style.active : null}
|
||||
>
|
||||
CLIENT
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowServer((s) => !s)}
|
||||
onAuxClick={() => disableOthers('SERVER')}
|
||||
className={(showServer) ? style.active : null}>
|
||||
className={showServer ? style.active : null}
|
||||
>
|
||||
SERVER
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowPlayback((s) => !s)}
|
||||
onAuxClick={() => disableOthers('PLAYBACK')}
|
||||
className={(showPlayback) ? style.active : null}>
|
||||
className={showPlayback ? style.active : null}
|
||||
>
|
||||
Playback
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowRx((s) => !s)}
|
||||
onAuxClick={() => disableOthers('RX')}
|
||||
className={(showRx) ? style.active : null}>
|
||||
className={showRx ? style.active : null}
|
||||
>
|
||||
RX
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowTx((s) => !s)}
|
||||
onAuxClick={() => disableOthers('TX')}
|
||||
className={(showTx) ? style.active : null}>
|
||||
className={showTx ? style.active : null}
|
||||
>
|
||||
TX
|
||||
</div>
|
||||
<div
|
||||
onClick={clearLog}
|
||||
className={style.clear}>
|
||||
<div onClick={clearLog} className={style.clear}>
|
||||
Clear
|
||||
</div>
|
||||
</div>
|
||||
</HStack>
|
||||
<ul className={style.log}>
|
||||
{data.map((d) => (
|
||||
<li key={d.id} className={d.level === 'INFO' ? style.info : d.level === 'WARN' ? style.warn : d.level === 'ERROR' ? style.error : ''}>
|
||||
<div
|
||||
className={style.time}
|
||||
>{d.time}</div>
|
||||
<li
|
||||
key={d.id}
|
||||
className={
|
||||
d.level === 'INFO'
|
||||
? style.info
|
||||
: d.level === 'WARN'
|
||||
? style.warn
|
||||
: d.level === 'ERROR'
|
||||
? style.error
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<div className={style.time}>{d.time}</div>
|
||||
<div className={style.origin}>{d.origin}</div>
|
||||
<div className={style.msg}>{d.text}</div>
|
||||
</li>
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
font-size: 0.8em;
|
||||
user-select:text;
|
||||
@include container-bg;
|
||||
user-select: text;
|
||||
@include third-container;
|
||||
padding: 0 0.5em;
|
||||
margin: 0 0.5em;
|
||||
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
@@ -36,39 +39,33 @@
|
||||
}
|
||||
|
||||
li.info {
|
||||
color: #aaa;
|
||||
color: $info-gray;
|
||||
}
|
||||
li.warn {
|
||||
color: #dd6b20;
|
||||
color: $warning-orange;
|
||||
}
|
||||
li.error {
|
||||
color: #f00;
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
.entry:hover {
|
||||
color: #ddd;
|
||||
li:hover {
|
||||
color: $info-gray-hover;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.info {
|
||||
color: #fff;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.client {
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
.toggleBar {
|
||||
display: flex;
|
||||
font-size: 0.7em;
|
||||
justify-content: flex-start;
|
||||
gap: 1em;
|
||||
padding: 0.5em 0;
|
||||
padding: 0.5em 8px;
|
||||
font-weight: 600;
|
||||
|
||||
div {
|
||||
@@ -85,6 +82,7 @@
|
||||
}
|
||||
|
||||
.clear {
|
||||
margin-left: auto;
|
||||
border: 1px solid rgba($ontime-pink, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,9 @@ export default function InfoNif() {
|
||||
isCollapsed={collapsed}
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
/>
|
||||
{!collapsed && (
|
||||
{!collapsed && (status === 'success') &&(
|
||||
<div>
|
||||
{status === 'success' &&
|
||||
data?.networkInterfaces.map((e) => (
|
||||
{data?.networkInterfaces.map((e) => (
|
||||
<a
|
||||
key={e.address}
|
||||
href='#!'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import style from './Info.module.scss';
|
||||
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
|
||||
|
||||
export default function InfoTitle(props) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
@@ -15,42 +14,28 @@ export default function InfoTitle(props) {
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<div className={roll ? style.headerRoll : style.header}>
|
||||
{title}
|
||||
{collapsed && (
|
||||
<span className={style.collapsedTitle}>{data.title}</span>
|
||||
)}
|
||||
<Icon
|
||||
className={collapsed ? style.moreCollapsed : style.moreExpanded}
|
||||
as={FiChevronUp}
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CollapseBar
|
||||
title={title}
|
||||
isCollapsed={collapsed}
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
roll={roll}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<>
|
||||
<div className={style.labelContainer}>
|
||||
<span className={noTitl ? style.emptyLabel : style.label}>
|
||||
Title
|
||||
</span>
|
||||
<span className={noTitl ? style.emptyLabel : style.label}>Title</span>
|
||||
{data.title}
|
||||
</div>
|
||||
<div className={style.labelContainer}>
|
||||
<span className={noPres ? style.emptyLabel : style.label}>
|
||||
Presenter
|
||||
</span>
|
||||
<span className={noPres ? style.emptyLabel : style.label}>Presenter</span>
|
||||
{data.presenter}
|
||||
</div>
|
||||
<div className={style.labelContainer}>
|
||||
<span className={noSubt ? style.emptyLabel : style.label}>
|
||||
Subtitle
|
||||
</span>
|
||||
<span className={noSubt ? style.emptyLabel : style.label}>Subtitle</span>
|
||||
{data.subtitle}
|
||||
</div>
|
||||
<div className={style.notes}>
|
||||
<span className={noNote ? style.emptyLabel : style.label}>
|
||||
Note
|
||||
</span>
|
||||
<span className={noNote ? style.emptyLabel : style.label}>Note</span>
|
||||
{data.note}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import { Divider } from '@chakra-ui/react';
|
||||
import { ButtonGroup, Divider, HStack } from '@chakra-ui/react';
|
||||
import { CursorContext } from '../../app/context/CursorContext';
|
||||
import MenuActionButtons from './MenuActionButtons';
|
||||
import CollapseBtn from 'common/components/buttons/CollapseBtn';
|
||||
@@ -42,20 +42,25 @@ const EventListMenu = ({ eventsHandler }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.headerButtons}>
|
||||
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
|
||||
<CollapseBtn size='sm' clickhandler={() => eventsHandler('collapseall')} />
|
||||
<HStack className={style.headerButtons}>
|
||||
<ButtonGroup isAttached>
|
||||
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
|
||||
<CollapseBtn size='sm' clickhandler={() => eventsHandler('collapseall')} />
|
||||
</ButtonGroup>
|
||||
<Divider orientation='vertical' />
|
||||
<CursorUpBtn size='sm' clickhandler={() => actionHandler('cursorUp')} />
|
||||
<CursorDownBtn size='sm' clickhandler={() => actionHandler('cursorDown')} />
|
||||
<CursorLockedBtn
|
||||
size='sm'
|
||||
clickhandler={() => actionHandler('togglelock')}
|
||||
active={isCursorLocked}
|
||||
/>
|
||||
<ButtonGroup isAttached>
|
||||
<CursorUpBtn size='sm' clickhandler={() => actionHandler('cursorUp')} />
|
||||
<CursorDownBtn size='sm' clickhandler={() => actionHandler('cursorDown')} />
|
||||
<CursorLockedBtn
|
||||
size='sm'
|
||||
clickhandler={() => actionHandler('togglelock')}
|
||||
active={isCursorLocked}
|
||||
width='3em'
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<Divider orientation='vertical' />
|
||||
<MenuActionButtons actionHandler={actionHandler} size='sm' />
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
.headerButtons {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
align-content: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.menu {
|
||||
color: #000;
|
||||
background-color: rgba(255, 255, 255, 0.67);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { downloadEvents, uploadEvents } from 'app/api/ontimeApi';
|
||||
import { EVENTS_TABLE } from 'app/api/apiConstants';
|
||||
@@ -12,9 +12,10 @@ import HelpIconBtn from './buttons/HelpIconBtn';
|
||||
import UploadIconBtn from './buttons/UploadIconBtn';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import PropTypes from 'prop-types';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
|
||||
export default function MenuBar(props) {
|
||||
const { isOpen, onOpen } = props;
|
||||
const { isOpen, onOpen, onClose } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const hiddenFileInput = useRef(null);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -35,7 +36,7 @@ export default function MenuBar(props) {
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
fontSize: '1.5em'
|
||||
fontSize: '1.5em',
|
||||
};
|
||||
|
||||
const handleUpload = (event) => {
|
||||
@@ -44,7 +45,7 @@ export default function MenuBar(props) {
|
||||
|
||||
// Limit file size to 1MB
|
||||
if (fileUploaded.size > 1000000) {
|
||||
emitError('Error: File size limit (1MB) exceeded')
|
||||
emitError('Error: File size limit (1MB) exceeded');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,10 +54,10 @@ export default function MenuBar(props) {
|
||||
try {
|
||||
uploaddb.mutate(fileUploaded);
|
||||
} catch (error) {
|
||||
emitError(`Failed uploading file: ${error}`)
|
||||
emitError(`Failed uploading file: ${error}`);
|
||||
}
|
||||
} else {
|
||||
emitError('Error: File type unknown')
|
||||
emitError('Error: File type unknown');
|
||||
}
|
||||
|
||||
// reset input value
|
||||
@@ -65,7 +66,7 @@ export default function MenuBar(props) {
|
||||
|
||||
const handleIPC = (action) => {
|
||||
// Stop crashes when testing locally
|
||||
if (window.process?.type === undefined) {
|
||||
if (typeof window.process?.type === 'undefined') {
|
||||
if (action === 'help') {
|
||||
window.open('https://cpvalente.gitbook.io/ontime/');
|
||||
}
|
||||
@@ -92,27 +93,45 @@ export default function MenuBar(props) {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// check if the alt key is pressed
|
||||
if (e.ctrlKey) {
|
||||
if (e.key === ',') {
|
||||
// if we are in electron
|
||||
if (window.process?.type === undefined) return;
|
||||
if (window.process.type === 'renderer') {
|
||||
// open if not open
|
||||
isOpen ? onClose() : onOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[isOpen, onClose, onOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VStack>
|
||||
<QuitIconBtn size='lg' clickhandler={() => handleIPC('shutdown')} />
|
||||
<MaxIconBtn
|
||||
style={{ ...buttonStyle }}
|
||||
size='lg'
|
||||
clickhandler={() => handleIPC('max')}
|
||||
/>
|
||||
<MinIconBtn
|
||||
style={{ ...buttonStyle }}
|
||||
size='lg'
|
||||
clickhandler={() => handleIPC('min')}
|
||||
/>
|
||||
<MaxIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('max')} />
|
||||
<MinIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('min')} />
|
||||
<div className={style.gap} />
|
||||
<HelpIconBtn
|
||||
style={{ ...buttonStyle }}
|
||||
size='lg'
|
||||
clickhandler={() => handleIPC('help')}
|
||||
/>
|
||||
<HelpIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('help')} />
|
||||
<SettingsIconBtn
|
||||
style={{...buttonStyle}}
|
||||
style={{ ...buttonStyle }}
|
||||
size='lg'
|
||||
className={isOpen ? style.open : ''}
|
||||
clickhandler={onOpen}
|
||||
@@ -126,22 +145,14 @@ export default function MenuBar(props) {
|
||||
onChange={handleUpload}
|
||||
accept='.json, .xlsx'
|
||||
/>
|
||||
<UploadIconBtn
|
||||
style={{ ...buttonStyle }}
|
||||
size='lg'
|
||||
clickhandler={handleClick}
|
||||
/>
|
||||
<DownloadIconBtn
|
||||
style={{ ...buttonStyle }}
|
||||
size='lg'
|
||||
clickhandler={handleDownload}
|
||||
/>
|
||||
</>
|
||||
<UploadIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={handleClick} />
|
||||
<DownloadIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={handleDownload} />
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
MenuBar.propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import MenuBar from "../MenuBar";
|
||||
import {queryClientMock} from "../../../__mocks__/QueryClient.mock";
|
||||
import {QueryClientProvider} from "react-query";
|
||||
import MenuBar from '../MenuBar';
|
||||
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
|
||||
const onOpenHandler = jest.fn();
|
||||
const onCloseHandler = jest.fn();
|
||||
|
||||
const renderInMock = () => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClientMock}>
|
||||
<MenuBar onOpen={onOpenHandler} />
|
||||
<MenuBar onOpen={onOpenHandler} onClose={onCloseHandler} />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
test('check that menu bar renders correctly', () => {
|
||||
// need to inject the react query provider
|
||||
renderInMock();
|
||||
|
||||
const nButtons = screen.getAllByRole("button").length;
|
||||
const nButtons = screen.getAllByRole('button').length;
|
||||
expect(nButtons).toBe(7);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import MenuBar from "../MenuBar";
|
||||
import {queryClientMock} from "../../../__mocks__/QueryClient.mock";
|
||||
import {QueryClientProvider} from "react-query";
|
||||
import MenuBar from '../MenuBar';
|
||||
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
|
||||
import { QueryClientProvider } from 'react-query';
|
||||
|
||||
const onOpenHandler = jest.fn();
|
||||
const onCloseHandler = jest.fn();
|
||||
|
||||
const renderInMock = () => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClientMock}>
|
||||
<MenuBar onOpen={onOpenHandler} />
|
||||
<MenuBar onOpen={onOpenHandler} onClose={onCloseHandler} />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
test('check that menu bar renders correctly', () => {
|
||||
// need to inject the react query provider
|
||||
renderInMock();
|
||||
|
||||
const nButtons = screen.getAllByRole("button").length;
|
||||
const nButtons = screen.getAllByRole('button').length;
|
||||
expect(nButtons).toBe(7);
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function AppSettingsModal() {
|
||||
|
||||
// we might not have changed this
|
||||
if (f.pinCode !== data.pinCode) {
|
||||
let e = { status: false, message: '' };
|
||||
const e = { status: false, message: '' };
|
||||
|
||||
// Validate fields
|
||||
if (f.pinCode === '' || f.pinCode == null) {
|
||||
|
||||
@@ -45,13 +45,11 @@ export default function IntegrationSettingsModal() {
|
||||
// set fields with error
|
||||
if (e.status) {
|
||||
emitError(`Invalid Input: ${e.message}`);
|
||||
return;
|
||||
} else {
|
||||
await postInfo(f);
|
||||
setChanged(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function OscSettingsModal() {
|
||||
setSubmitting(true);
|
||||
|
||||
const f = formData;
|
||||
let e = { status: false, message: '' };
|
||||
const e = { status: false, message: '' };
|
||||
|
||||
// Validate fields
|
||||
if (f.port < 1024 || f.port > 65535) {
|
||||
@@ -137,7 +137,7 @@ export default function OscSettingsModal() {
|
||||
/**
|
||||
* Handles change of input field in local state
|
||||
* @param {string} field - object parameter to update
|
||||
* @param {(string | number)} value - new object parameter value
|
||||
* @param {(string | number | boolean)} value - new object parameter value
|
||||
*/
|
||||
const handleChange = (field, value) => {
|
||||
const temp = { ...formData };
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function TableOptionsModal() {
|
||||
|
||||
// validation step makes clean string
|
||||
const validatedFields = { ...userFields };
|
||||
let errors = false;
|
||||
const errors = false;
|
||||
for (const field in validatedFields) {
|
||||
validatedFields[field] = validatedFields[field].trim();
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
|
||||
import { IoMoon } from '@react-icons/all-files/io5/IoMoon';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
import { useSocket } from '../../app/context/socketContext';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { formatDisplay } from '../../common/utils/dateConfig';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import PlaybackIcon from './tableElements/PlaybackIcon';
|
||||
import { stringFromMillis } from '../../common/utils/time';
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function TableHeader() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import EditableCell from './tableElements/EditableCell';
|
||||
import { stringFromMillis } from '../../common/utils/time.js';
|
||||
|
||||
/**
|
||||
* React - Table column object
|
||||
|
||||
@@ -3,9 +3,9 @@ import React, { useEffect, useState } from 'react';
|
||||
import { fetchAllEvents } from 'app/api/eventsApi';
|
||||
import { fetchEvent } from 'app/api/eventApi';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { useFetch } from 'app/hooks/useFetch';
|
||||
import { EVENT_TABLE, EVENTS_TABLE } from 'app/api/apiConstants';
|
||||
import { stringFromMillis } from '../../common/utils/time';
|
||||
|
||||
const withSocket = (Component) => {
|
||||
return (props) => {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import style from './StageManager.module.css';
|
||||
import Paginator from 'common/components/views/Paginator';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import TitleSide from 'common/components/views/TitleSide';
|
||||
import {getEventsWithDelay} from "../../../common/utils/eventsManager";
|
||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import style from './StageManager.module.scss';
|
||||
|
||||
export default function StageManager(props) {
|
||||
const { publ, title, time, backstageEvents, selectedId, general } = props;
|
||||
const [filteredEvents, setFilteredEvents] = useState(null);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -20,11 +23,9 @@ export default function StageManager(props) {
|
||||
// calculate delays if any
|
||||
useEffect(() => {
|
||||
if (backstageEvents == null) return;
|
||||
|
||||
const f = getEventsWithDelay(backstageEvents)
|
||||
|
||||
const f = getEventsWithDelay(backstageEvents);
|
||||
setFilteredEvents(f);
|
||||
}, [backstageEvents]);
|
||||
}, [backstageEvents]);
|
||||
|
||||
// Format messages
|
||||
const showPubl = publ.text !== '' && publ.visible;
|
||||
@@ -37,22 +38,6 @@ export default function StageManager(props) {
|
||||
if (time.isNegative) stageTimer = `-${stageTimer}`;
|
||||
}
|
||||
|
||||
// motion
|
||||
const titleVariants = {
|
||||
hidden: {
|
||||
x: -1500,
|
||||
},
|
||||
visible: {
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 1,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
x: -1500,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.container__gray}>
|
||||
<NavLogo />
|
||||
@@ -102,17 +87,28 @@ export default function StageManager(props) {
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={style.todayContainer}>
|
||||
<div className={style.label}>Today</div>
|
||||
<div className={style.entriesContainer}>
|
||||
<Paginator selectedId={selectedId} events={filteredEvents} isBackstage />
|
||||
<div className={style.todayHeaderBlock}>
|
||||
<div className={style.label}>Today</div>
|
||||
<div className={style.nav}>
|
||||
{pageNumber > 1 &&
|
||||
[...Array(pageNumber).keys()].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === currentPage ? style.navItemSelected : style.navItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Paginator
|
||||
selectedId={selectedId}
|
||||
events={filteredEvents}
|
||||
isBackstage
|
||||
setCurrentPage={setCurrentPage}
|
||||
setPageNumber={setPageNumber}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
showPubl ? style.publicContainer : style.publicContainerHidden
|
||||
}
|
||||
>
|
||||
<div className={showPubl ? style.publicContainer : style.publicContainerHidden}>
|
||||
<div className={style.label}>Public message</div>
|
||||
<div className={style.message}>{publ.text}</div>
|
||||
</div>
|
||||
@@ -134,11 +130,7 @@ export default function StageManager(props) {
|
||||
</div>
|
||||
<div className={style.qr}>
|
||||
{general.url != null && general.url !== '' && (
|
||||
<QRCode
|
||||
value={general.url}
|
||||
size={window.innerWidth / 12}
|
||||
level='L'
|
||||
/>
|
||||
<QRCode value={general.url} size={window.innerWidth / 12} level='L' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
.container__gray,
|
||||
.container__grayFinished {
|
||||
margin: 0;
|
||||
box-sizing: border-box; /* reset */
|
||||
overflow: hidden;
|
||||
width: 100%; /* restrict the page width to viewport */
|
||||
|
||||
background: linear-gradient(90deg, #252525 0%, #121212 100%);
|
||||
height: 100vh;
|
||||
color: #fffd;
|
||||
font-weight: 300;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 3vw 2fr;
|
||||
grid-template-rows: 15vh 1fr 1fr 0 13vh 13vh;
|
||||
grid-template-areas:
|
||||
' titl titl titl . schd'
|
||||
' now now now . schd'
|
||||
' next next .... . schd'
|
||||
' ... .... .... . ....'
|
||||
' publ publ clck . info'
|
||||
' publ publ time . info';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
grid-area: titl;
|
||||
font-size: 3vw;
|
||||
font-weight: 600;
|
||||
text-decoration: underline #ff7597 0.5vh;
|
||||
padding-top: 0.2vh;
|
||||
padding-left: 1vw;
|
||||
}
|
||||
|
||||
/* =================== TITLES ===================*/
|
||||
|
||||
.infoContainer > div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nextContainer > div,
|
||||
.nowContainer > div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nowContainer,
|
||||
.nextContainer,
|
||||
.todayContainer {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
padding: 1vh 2vw;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clockContainer,
|
||||
.countdownContainer,
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
padding: 1vh 1vw;
|
||||
}
|
||||
|
||||
.publicContainer {
|
||||
opacity: 1;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.publicContainerHidden {
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
.todayContainer,
|
||||
.infoContainer {
|
||||
background-color: rgba(255, 255, 255, 0.07);
|
||||
padding: 2.5vh 2vw;
|
||||
}
|
||||
|
||||
.infoContainer,
|
||||
.todayContainer,
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
border-radius: 1vw;
|
||||
}
|
||||
|
||||
.nowContainer,
|
||||
.nextContainer {
|
||||
margin-left: -1vw;
|
||||
}
|
||||
|
||||
.nowContainer {
|
||||
background-color: rgba(255, 255, 255, 0.09);
|
||||
grid-area: now;
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
}
|
||||
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
grid-area: publ;
|
||||
}
|
||||
|
||||
.nextContainer {
|
||||
grid-area: next;
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
}
|
||||
|
||||
/* =================== SCHEDULE ===================*/
|
||||
|
||||
.todayContainer {
|
||||
grid-area: schd;
|
||||
display: grid;
|
||||
grid-template-rows: 5vh 1fr 3vh;
|
||||
margin-top: 3vh;
|
||||
height: 95%;
|
||||
}
|
||||
|
||||
/* =================== OVERLAY ===================*/
|
||||
|
||||
.message {
|
||||
font-size: 3vw;
|
||||
line-height: 3vw;
|
||||
padding: 0.5vh 0 0.5vh 1vw;
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
grid-area: info;
|
||||
display: grid;
|
||||
|
||||
grid-template-rows: 3vh minmax(0, 1fr);
|
||||
grid-template-columns: 3fr 1fr;
|
||||
grid-template-areas:
|
||||
'titl .'
|
||||
'binf qr';
|
||||
gap: 0.5vw;
|
||||
}
|
||||
|
||||
.infoMessages {
|
||||
grid-area: binf;
|
||||
font-size: 1.5vw;
|
||||
line-height: 2vw;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.qr {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
grid-area: qr;
|
||||
}
|
||||
|
||||
/* =================== MAIN ===================*/
|
||||
|
||||
.clockContainer {
|
||||
grid-area: time;
|
||||
border-radius: 0 0 1vw 1vw;
|
||||
}
|
||||
|
||||
.countdownContainer {
|
||||
grid-area: clck;
|
||||
border-radius: 1vw 1vw 0 0;
|
||||
}
|
||||
|
||||
.clock {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 3vw;
|
||||
line-height: 3vw;
|
||||
text-align: center;
|
||||
letter-spacing: 0.25vw;
|
||||
color: #ddd;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
@use '../../../styles/main' as *;
|
||||
@use '../../../styles/viewers' as *;
|
||||
|
||||
.container__gray,
|
||||
.container__grayFinished {
|
||||
@include viewer-container;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 3vw 2fr;
|
||||
grid-template-rows: 15vh 1fr 1fr 0 13vh 13vh;
|
||||
grid-template-areas:
|
||||
' titl titl titl . schd'
|
||||
' now now now . schd'
|
||||
' next next .... . schd'
|
||||
' ... .... .... . ....'
|
||||
' publ publ clck . info'
|
||||
' publ publ time . info';
|
||||
gap: 1vw;
|
||||
padding: 1vw;
|
||||
}
|
||||
|
||||
.eventTitle {
|
||||
grid-area: titl;
|
||||
@include viewer-event-title;
|
||||
}
|
||||
|
||||
.nowContainer {
|
||||
grid-area: now;
|
||||
background-color: $bg-gray-950;
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
}
|
||||
|
||||
.publicContainer,
|
||||
.publicContainerHidden {
|
||||
grid-area: publ;
|
||||
}
|
||||
|
||||
.nextContainer {
|
||||
grid-area: next;
|
||||
border-radius: 0 2vw 2vw 0;
|
||||
}
|
||||
|
||||
.todayContainer {
|
||||
grid-area: schd;
|
||||
}
|
||||
|
||||
.infoContainer {
|
||||
grid-area: info;
|
||||
}
|
||||
|
||||
.clockContainer {
|
||||
grid-area: time;
|
||||
border-radius: 0 0 1vw 1vw;
|
||||
}
|
||||
|
||||
.countdownContainer {
|
||||
grid-area: clck;
|
||||
border-radius: 1vw 1vw 0 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user