* 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:
Carlos Valente
2022-04-17 20:30:39 +02:00
committed by GitHub
parent 20d3ddbc18
commit db0eee3b9c
140 changed files with 10160 additions and 2139 deletions
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
+166
View File
@@ -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 }}
+1 -18
View File
@@ -19,20 +19,7 @@ jobs:
with: with:
node-version: '14.x' 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 # React
- name: React - Link to utils
run: yarn link ontime-utils
working-directory: ./client
- name: React - Install dependencies - name: React - Install dependencies
run: yarn install run: yarn install
working-directory: ./client working-directory: ./client
@@ -46,17 +33,13 @@ jobs:
working-directory: ./client working-directory: ./client
# Node server # Node server
- name: React - Link to utils
run: yarn link ontime-utils
working-directory: ./server/src
- name: Server - Install dependencies - name: Server - Install dependencies
run: yarn install run: yarn install
working-directory: ./server/src working-directory: ./server/src
# App # App
- name: Electron - Install dependencies - name: Electron - Install dependencies
run: yarn install && yarn make && yarn setdb run: yarn install && yarn setdb
working-directory: ./server working-directory: ./server
- name: Electron - Run tests - name: Electron - Run tests
+31 -8
View File
@@ -2,7 +2,11 @@
Download the latest releases here Download the latest releases here
- [Windows](https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe) <div style="display: flex; justify-content: space-around">
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/mac-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/win-download.png"/></a>
<img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/>
</div>
# Ontime # Ontime
Ontime is an application for managing event rundowns and running stage timers. 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... For the presentation views...
------------------------------------------------------------- -------------------------------------------------------------
IP.ADDRESS:4001 > Web server default to presenter timer view 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/sm > Stage Manager / Backstage view
IP.ADDRESS:4001/public > Public / Foyer view IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/pip > Picture in Picture 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 ### Continued development
There are several features planned in the roadmap. 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. 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 - [ ] Linux version
- [ ] Headless version (run server only anywhere, configure from a browser locally) - [ ] Headless version (run server only anywhere, configure from a browser locally)
- [ ] Companion module - [ ] Companion module
- [ ] Lower Third Manager - [ ] Lower Third Manager
- [ ] Note only event - [ ] Note only event
- [ ] Reach Schedule: way to speedup timer to meet a deadline - [ ] Reach Schedule: way to speedup timer to meet a deadline
- [ ] vMix integration
### For version 1 ### For version 1
Almost reaching a feature set that we can call v1. Before that: Almost reaching a feature set that we can call v1. Before that:
- [ ] Mac OS version - [ ] Mac OS version
### Issues ### Issues
The app is still in pre-release and there are a few issues, mainly concerning style. The app is still in pre-release and there are a few issues, mainly concerning responsiveness in
This will be receiving attention as we near v1 release different screens. If you run into problems, please open an issue with a screenshot and your device
resolution
#### Style #### Unsigned App
- [ ] 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 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
Help is underway! ... and can be viewed [here](https://cpvalente.gitbook.io/ontime/) Help is underway! ... and can be viewed [here](https://cpvalente.gitbook.io/ontime/)
-1
View File
@@ -14,7 +14,6 @@
"axios": "^0.25.0", "axios": "^0.25.0",
"color": "^4.2.1", "color": "^4.2.1",
"framer-motion": "4.1.17", "framer-motion": "4.1.17",
"ontime-utils": "link: ../server/utils/",
"react": "17.0.2", "react": "17.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
+1 -4
View File
@@ -6,7 +6,4 @@ export const fetchEvent = async () => {
return res.data; return res.data;
}; };
export const postEvent = async (data) => { export const postEvent = async (data) => axios.post(eventURL, data);
const res = await axios.post(eventURL, data);
return res;
};
+7 -22
View File
@@ -6,31 +6,16 @@ export const fetchAllEvents = async () => {
return res.data; return res.data;
}; };
export const requestPost = async (data) => { export const requestPost = async (data) => axios.post(eventsURL, data);
await axios.post(eventsURL, data);
};
export const requestPut = async (data) => { export const requestPut = async (data) => axios.put(eventsURL, data);
await axios.put(eventsURL, data);
};
export const requestPatch = async (data) => { export const requestPatch = async (data) => axios.patch(eventsURL, data);
await axios.patch(eventsURL, data);
};
export const requestReorder = async (data) => { export const requestReorder = async (data) => axios.patch(`${eventsURL}/reorder`, data);
await axios.patch(`${eventsURL}/reorder`, data);
};
export const requestApplyDelay = async (eventId) => { export const requestApplyDelay = async (eventId) => axios.patch(`${eventsURL}/applydelay/${eventId}`);
const action = 'applydelay';
return await axios.patch(`${eventsURL}/${action}/${eventId}`);
};
export const requestDelete = async (eventId) => { export const requestDelete = async (eventId) => axios.delete(`${eventsURL}/${eventId}`);
await axios.delete(`${eventsURL}/${eventId}`);
};
export const requestDeleteAll = async () => { export const requestDeleteAll = async () => axios.delete(`${eventsURL}/all`);
await axios.delete(`${eventsURL}/all`);
};
+14 -28
View File
@@ -104,46 +104,35 @@ export const getSettings = async () => {
return res.data; return res.data;
}; };
export const postSettings = async (data) => { export const postSettings = async (data) => axios.post(`${ontimeURL}/settings`, data);
await axios.post(`${ontimeURL}/settings`, data);
};
export const getInfo = async () => { export const getInfo = async () => {
const res = await axios.get(`${ontimeURL}/info`); const res = await axios.get(`${ontimeURL}/info`);
return res.data; return res.data;
}; };
export const postInfo = async (data) => { export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
await axios.post(`${ontimeURL}/info`, data);
};
export const getAliases = async () => { export const getAliases = async () => {
const res = await axios.get(`${ontimeURL}/aliases`); const res = await axios.get(`${ontimeURL}/aliases`);
return res.data; return res.data;
}; };
export const postAliases = async (data) => { export const postAliases = async (data) => axios.post(`${ontimeURL}/aliases`, data);
await axios.post(`${ontimeURL}/aliases`, data);
};
export const getUserFields = async () => { export const getUserFields = async () => {
const res = await axios.get(`${ontimeURL}/userfields`); const res = await axios.get(`${ontimeURL}/userfields`);
return res.data; return res.data;
}; };
export const postUserFields = async (data) => { export const postUserFields = async (data) => axios.post(`${ontimeURL}/userfields`, data);
await axios.post(`${ontimeURL}/userfields`, data);
};
export const getOSC = async () => { export const getOSC = async () => {
const res = await axios.get(`${ontimeURL}/osc`); const res = await axios.get(`${ontimeURL}/osc`);
return res.data; return res.data;
}; };
export const postOSC = async (data) => { export const postOSC = async (data) => axios.post(`${ontimeURL}/osc`, data);
await axios.post(`${ontimeURL}/osc`, data);
};
export const downloadEvents = async () => { export const downloadEvents = async () => {
await axios({ await axios({
@@ -151,13 +140,13 @@ export const downloadEvents = async () => {
method: 'GET', method: 'GET',
responseType: 'blob', // important responseType: 'blob', // important
}).then((response) => { }).then((response) => {
let headerLine = response.headers['Content-Disposition']; const headerLine = response.headers['Content-Disposition'];
let filename = 'events.json'; let filename = 'events.json';
// try and get the filename from the response // try and get the filename from the response
if (headerLine != null) { if (headerLine != null) {
let startFileNameIndex = headerLine.indexOf('"') + 1; const startFileNameIndex = headerLine.indexOf('"') + 1;
let endFileNameIndex = headerLine.lastIndexOf('"'); const endFileNameIndex = headerLine.lastIndexOf('"');
filename = headerLine.substring(startFileNameIndex, endFileNameIndex); filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
} }
@@ -173,14 +162,11 @@ export const downloadEvents = async () => {
export const uploadEvents = async (file) => { export const uploadEvents = async (file) => {
const formData = new FormData(); const formData = new FormData();
formData.append('userFile', file); // appending file formData.append('userFile', file); // appending file
await axios await axios.post(`${ontimeURL}/db`, formData, {
.post(`${ontimeURL}/db`, formData, { headers: {
headers: { 'Content-Type': 'multipart/form-data',
'Content-Type': 'multipart/form-data', },
}, });
});
}; };
export const uploadEventsWithPath = async (filepath) => { export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath });
await axios.post(`${ontimeURL}/dbpath`, { path: filepath });
};
+8 -29
View File
@@ -1,37 +1,16 @@
import axios from 'axios'; import axios from 'axios';
import { playbackURL } from '../api/apiConstants'; import { playbackURL } from './apiConstants';
export const getStart = async () => { export const getStart = async () => axios.get(`${playbackURL}/start`);
const res = await axios.get(playbackURL + '/start');
return res;
};
export const getPause = async () => { export const getPause = async () => axios.get(`${playbackURL}/pause`);
const res = axios.get(playbackURL + '/pause');
return res;
};
export const getRoll = async () => { export const getRoll = async () => axios.get(`${playbackURL}/roll`);
const res = axios.get(playbackURL + '/roll');
return res;
};
export const getPrevious = async () => { export const getPrevious = async () => axios.get(`${playbackURL}/previous`);
const res = axios.get(playbackURL + '/previous');
return res;
};
export const getNext = async () => { export const getNext = async () => axios.get(`${playbackURL}/next`);
const res = axios.get(playbackURL + '/next');
return res;
};
export const getUnload = async () => { export const getUnload = async () => axios.get(`${playbackURL}/unload`);
const res = axios.get(playbackURL + '/unload');
return res;
};
export const getReload = async () => { export const getReload = async () => axios.get(`${playbackURL}/reload`);
const res = axios.get(playbackURL + '/reload');
return res;
};
+1 -1
View File
@@ -31,7 +31,7 @@ export const CursorProvider = ({ children }) => {
*/ */
const toggleCursorLocked = useCallback( const toggleCursorLocked = useCallback(
(newValue = undefined) => { (newValue = undefined) => {
if (newValue === undefined) { if (typeof newValue === 'undefined') {
if (isCursorLocked) { if (isCursorLocked) {
cursorLockedOff(); cursorLockedOff();
} else { } else {
+2 -2
View File
@@ -1,7 +1,7 @@
import React, { createContext, useCallback, useEffect, useState } from 'react'; import React, { createContext, useCallback, useEffect, useState } from 'react';
import { useSocket } from './socketContext'; import { useSocket } from './socketContext';
import { generateId } from 'ontime-utils/generate_id'; import { generateId } from '../../common/utils/generate_id';
import { nowInMillis, stringFromMillis } from 'ontime-utils/time'; import { nowInMillis, stringFromMillis } from '../../common/utils/time';
export const LoggingContext = createContext({ export const LoggingContext = createContext({
logData: [], logData: [],
+1 -1
View File
@@ -5,7 +5,7 @@
* @returns {{catch: string, value: boolean}} * @returns {{catch: string, value: boolean}}
*/ */
export const validateTimes = (timeStart, timeEnd) => { export const validateTimes = (timeStart, timeEnd) => {
let validate = { value: true, catch: '' }; const validate = { value: true, catch: '' };
if (timeStart > timeEnd) { if (timeStart > timeEnd) {
validate.catch = 'Start time later than end time'; validate.catch = 'Start time later than end time';
} }
@@ -12,7 +12,6 @@ export default function CollapseBtn(props) {
icon={<FiChevronsUp />} icon={<FiChevronsUp />}
colorScheme='white' colorScheme='white'
variant='outline' variant='outline'
background='#fff1'
onClick={clickhandler} onClick={clickhandler}
_focus={{ boxShadow: 'none' }} _focus={{ boxShadow: 'none' }}
/> />
@@ -4,7 +4,7 @@ import { Tooltip } from '@chakra-ui/tooltip';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget'; import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
export default function CursorLockedBtn(props) { export default function CursorLockedBtn(props) {
const { clickhandler, active, ref, size } = props; const { clickhandler, active, ref, size, ...rest } = props;
return ( return (
<Tooltip label='Lock cursor to current'> <Tooltip label='Lock cursor to current'>
<IconButton <IconButton
@@ -17,6 +17,7 @@ export default function CursorLockedBtn(props) {
variant={active ? 'solid' : 'outline'} variant={active ? 'solid' : 'outline'}
onClick={clickhandler} onClick={clickhandler}
_focus={{ boxShadow: 'none' }} _focus={{ boxShadow: 'none' }}
{...rest}
/> />
</Tooltip> </Tooltip>
); );
@@ -1,6 +1,5 @@
import React from 'react'; import React, { useState } from 'react';
import { IconButton } from '@chakra-ui/button'; import { IconButton } from '@chakra-ui/button';
import { useState } from 'react';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove'; import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
@@ -12,7 +12,6 @@ export default function ExpandBtn(props) {
icon={<FiChevronsDown />} icon={<FiChevronsDown />}
colorScheme='white' colorScheme='white'
variant='outline' variant='outline'
background='#fff1'
onClick={clickhandler} onClick={clickhandler}
_focus={{ boxShadow: 'none' }} _focus={{ boxShadow: 'none' }}
/> />
@@ -1,14 +1,14 @@
import React from 'react'; import React from 'react';
import PropTypes from "prop-types"; import PropTypes from 'prop-types';
import style from "../../../features/info/Info.module.scss"; import { Icon } from '@chakra-ui/react';
import {Icon} from "@chakra-ui/react"; import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import {FiChevronUp} from "@react-icons/all-files/fi/FiChevronUp"; import style from './CollapseBar.module.scss';
export default function CollapseBar(props) { export default function CollapseBar(props) {
const {title = 'Collapse bar', isCollapsed = false, onClick}= props; const { title = 'Collapse bar', isCollapsed, onClick, roll } = props;
return( return (
<div className={style.header}> <div className={roll ? style.headerRoll : style.header}>
{title} {title}
<Icon <Icon
className={isCollapsed ? style.moreCollapsed : style.moreExpanded} className={isCollapsed ? style.moreCollapsed : style.moreExpanded}
@@ -16,10 +16,11 @@ export default function CollapseBar(props) {
onClick={onClick} onClick={onClick}
/> />
</div> </div>
) );
} }
CollapseBar.propTypes = { CollapseBar.propTypes = {
title: PropTypes.string, title: PropTypes.string,
isCollapsed: PropTypes.bool, isCollapsed: PropTypes.bool,
onClick: PropTypes.func, onClick: PropTypes.func,
} roll: PropTypes.bool,
};
@@ -1,17 +1,34 @@
@use '../../../styles/main' as *;
.header, .header,
.header__roll { .headerRoll {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-size: 0.9em; font-size: 0.9em;
color: #ccc;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.header { .header {
color: #ccc; color: $header-gray;
} }
.header__roll { .headerRoll {
color: #2b6cb0; 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 React, { memo } from 'react';
import { formatDisplay } from 'common/utils/dateConfig'; import { formatDisplay } from 'common/utils/dateConfig';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styles from './Countdown.module.css'; import styles from './Countdown.module.scss';
const Countdown = ({ time, small, isNegative, hideZeroHours }) => { const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
// prepare display string // prepare display string
const display = const display =
time != null && !isNaN(time) time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
? formatDisplay(time, hideZeroHours)
: '-- : -- : --';
const colour = isNegative ? '#ff7597' : '#fffffa'; const classes = `${small ? styles.countdownClockSmall : styles.countdownClock}
${isNegative ? styles.negative : ''}`;
return ( return (
<div <div className={classes}>
className={small ? styles.countdownClockSmall : styles.countdownClock}
style={{ color: colour }}
>
{display} {display}
</div> </div>
); );
@@ -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 */ /* Common */
.countdownClock, .countdownClock,
.countdownClockSmall { .countdownClockSmall {
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
line-height: 0.9; line-height: 0.9;
color: $text-white;
} }
/* Viewers */ /* Viewers */
@@ -21,5 +22,8 @@
text-align: center; text-align: center;
letter-spacing: 0.1em; letter-spacing: 0.1em;
font-weight: 600; font-weight: 600;
color: #fffffa; }
.negative {
color: $ontime-pink;
} }
@@ -1,9 +1,11 @@
/* eslint-disable react/destructuring-assignment */ /* eslint-disable react/destructuring-assignment */
import React from 'react'; import React from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext'; import { LoggingContext } from '../../../app/context/LoggingContext';
import style from './ErrorBoundary.module.scss';
class ErrorBoundary extends React.Component { class ErrorBoundary extends React.Component {
static contextType = LoggingContext; static contextType = LoggingContext;
reportContent = '';
constructor(props) { constructor(props) {
super(props); super(props);
@@ -21,11 +23,25 @@ class ErrorBoundary extends React.Component {
errorInfo: info, errorInfo: info,
}); });
this.context.emitError(error.toString()); this.context.emitError(error.toString());
this.reportContent = `${error} ${info.componentStack}`;
} }
render() { render() {
if (this.state.errorMessage) { 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; 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 React, { useContext } from 'react';
import EditableTimer from 'common/input/EditableTimer';
import { stringFromMillis } from 'ontime-utils/time';
import { LoggingContext } from '../../../app/context/LoggingContext'; import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator'; import { validateTimes } from '../../../app/entryValidator';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TimesDelayed from './TimesDelayed';
const label = { import Times from './Times';
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,
};
export default function EventTimesVertical(props) { export default function EventTimesVertical(props) {
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props; const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
@@ -129,11 +26,7 @@ export default function EventTimesVertical(props) {
start = val; start = val;
} else if (entry === 'timeEnd') { } else if (entry === 'timeEnd') {
end = val; end = val;
} else if (entry === 'durationOverride') { } else return entry === 'durationOverride';
return true;
} else {
return false;
}
const valid = validateTimes(start, end); const valid = validateTimes(start, end);
// give warning but not enforce validation // 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 React from 'react';
import { clamp } from 'app/utils/math'; import { clamp } from 'app/utils/math';
import styles from './MyProgressBar.module.css'; import styles from './MyProgressBar.module.scss';
export default function MyProgressBar(props) { export default function MyProgressBar(props) {
const { now, complete, showElapsed } = props; const { now, complete, showElapsed } = props;
@@ -1,22 +1,23 @@
@use '../../../styles/main' as *;
.progress, .progress,
.progressCountdown { .progressCountdown {
height: 2vh; height: 2vh;
background-color: rgba(255, 255, 255, 0.13);
border-radius: 4px; border-radius: 4px;
} }
.progress { .progress {
background-color: rgba(255, 255, 255, 0.13); background-color: $bg-gray-900;
} }
.progressCountdown { .progressCountdown {
background-color: #ff7597; background-color: $ontime-pink;
} }
.progressBar { .progressBar {
width: 100%; width: 100%;
height: 2vh; height: 2vh;
background-color: rgb(255, 255, 255); background-color: $title-white;
border-radius: 4px; border-radius: 4px;
transition: 1s linear; transition: 1s linear;
transition-property: width; transition-property: width;
+17 -48
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from "prop-types"; import PropTypes from 'prop-types';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Image } from '@chakra-ui/react'; import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion'; 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'; import style from './NavLogo.module.scss';
export default function NavLogo(props) { export default function NavLogo(props) {
const {isHidden} = props; const { isHidden } = props;
const [showNav, setShowNav] = useState(false); const [showNav, setShowNav] = useState(false);
const handleClick = () => { const handleClick = () => {
@@ -34,74 +34,43 @@ export default function NavLogo(props) {
}; };
}, [handleKeyPress]); }, [handleKeyPress]);
const baseOpacity = (isHidden) ? 0 : 0.5 const baseOpacity = isHidden ? 0 : 0.5;
return ( return (
<motion.div <motion.div
initial={{ opacity: baseOpacity }} initial={{ opacity: showNav ? 0.5 : baseOpacity }}
whileHover={{ opacity: 1 }} whileHover={{ opacity: 1 }}
className={style.navContainer} className={style.navContainer}
> >
<Image <Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
alt=''
src={navlogo}
className={style.logo}
onClick={handleClick}
/>
<AnimatePresence> <AnimatePresence>
{showNav && ( {showNav && (
<motion.div <motion.div
initial={{ opacity: 0, scale: 0, y: -50 }} initial={{ opacity: 0, scale: 0, y: -50 }}
animate={{ opacity: 1, scale: 1, y: 0 }} animate={{ opacity: 1, scale: 1, y: 0 }}
whileInView={{ opacity: 1 }}
exit={{ opacity: 0, scaleY: 0, y: -50 }} exit={{ opacity: 0, scaleY: 0, y: -50 }}
className={showNav ? style.nav : style.navHidden} className={style.nav}
> >
<Link <Link to='/timer' className={style.navItem} tabIndex={1}>
to='/timer'
className={style.navItem}
tabIndex={1}
>
Timer Timer
</Link> </Link>
<Link <Link to='/minimal' className={style.navItem} tabIndex={2}>
to='/minimal'
className={style.navItem}
tabIndex={2}
>
Minimal Timer Minimal Timer
</Link> </Link>
<Link <Link to='/sm' className={style.navItem} tabIndex={3}>
to='/sm'
className={style.navItem}
tabIndex={3}
>
Backstage Backstage
</Link> </Link>
<Link <Link to='/public' className={style.navItem} tabIndex={4}>
to='/public'
className={style.navItem}
tabIndex={4}
>
Public Public
</Link> </Link>
<Link <Link to='/lower' className={style.navItem} tabIndex={5}>
to='/lower'
className={style.navItem}
tabIndex={5}
>
Lower Thirds Lower Thirds
</Link> </Link>
<Link <Link to='/pip' className={style.navItem} tabIndex={6}>
to='/pip'
className={style.navItem}
tabIndex={6}
>
PIP PIP
</Link> </Link>
<Link <Link to='/studio' className={style.navItem} tabIndex={7}>
to='/studio'
className={style.navItem}
tabIndex={7}
>
Studio Clock Studio Clock
</Link> </Link>
</motion.div> </motion.div>
@@ -113,4 +82,4 @@ export default function NavLogo(props) {
NavLogo.propTypes = { NavLogo.propTypes = {
isHidden: PropTypes.bool, isHidden: PropTypes.bool,
} };
@@ -1,4 +1,4 @@
$nav-color: #fff; @use '../../../styles/main' as *;
.navContainer { .navContainer {
position: absolute; position: absolute;
@@ -9,22 +9,24 @@ $nav-color: #fff;
align-items: flex-end; align-items: flex-end;
font-size: max(1vw, 16px); font-size: max(1vw, 16px);
z-index: 10; z-index: 10;
}
.logo { .logo {
width: max(3vw, 32px); width: max(3vw, 32px);
opacity: 0.5; opacity: 0.5;
} cursor: pointer;
}
.nav { .nav,
gap: 2vh; .showNav {
display: flex; gap: 2vh;
flex-direction: column; display: flex;
} flex-direction: column;
.navItem { .navItem {
background-color: rgba(0, 0, 0, 0.7); background-color: $bg-overlay;
padding: 0.5vh 1vw; padding: 0.5vh 1vw;
border-radius: 4px; border-radius: 4px;
color: $nav-color; 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 PropTypes from 'prop-types';
import style from './ProtectRoute.module.scss'; 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 { IconButton } from '@chakra-ui/button';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck'; import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { AppContext } from '../../../app/context/AppContext'; import { AppContext } from '../../../app/context/AppContext';
@@ -13,53 +13,77 @@ export default function ProtectRoute({ children }) {
const [failed, setFailed] = useState(false); const [failed, setFailed] = useState(false);
const { auth, validate } = useContext(AppContext); const { auth, validate } = useContext(AppContext);
const handleValidation = useCallback(() => {
const r = validate(pin);
if (!r) {
setFailed(true);
setPin('');
}
}, [pin, validate]);
// Set window title // Set window title
useEffect(() => { useEffect(() => {
document.title = 'ontime'; document.title = 'ontime';
}, []); }, []);
const handleValidation = () => { // Handle keyboard shortcuts
const r = validate(pin); const handleKeyPress = useCallback(
if (!r) { (e) => {
setFailed(true); // handle held key
} if (e.repeat) return;
}; // Space bar
if (e.keyCode === 13) {
return ( handleValidation();
<> }
{!isLocal && !auth ? ( },
<div className={style.container}> [handleValidation],
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
)}
</>
); );
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 = { ProtectRoute.propTypes = {
@@ -2,7 +2,7 @@
@use '../../../styles/mixins' as *; @use '../../../styles/mixins' as *;
.container { .container {
background: #222; background: $bg-black;
display: grid; display: grid;
place-content: center; place-content: center;
@@ -18,8 +18,6 @@
.pin, .pin,
.pin__failed { .pin__failed {
display: flex;
gap: 10px;
padding: 20px; padding: 20px;
input { input {
@@ -1,33 +1,53 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import TodayItem from './TodayItem'; import TodayItem from './TodayItem';
import style from './Paginator.module.css';
import { useInterval } from 'app/hooks/useInterval'; import { useInterval } from 'app/hooks/useInterval';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import style from './Paginator.module.scss';
import Empty from '../../state/Empty';
export default function Paginator(props) { 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 LIMIT_PER_PAGE = limit;
const SCROLL_TIME = time * 1000 || 10000; const SCROLL_TIME = time * 1000;
const [numEvents, setNumEvents] = useState(0); const [numEvents, setNumEvents] = useState(0);
const [page, setPage] = useState([]); const [page, setPage] = useState([]);
const [pages, setPages] = useState(0); const [pages, setPages] = useState(0);
const [selPage, setSelPage] = 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(() => { useEffect(() => {
if (events == null) return; if (events == null) return;
// how many events in list // how many events in list
let n = events.length; const n = events.length;
setNumEvents(n); setNumEvents(n);
// how many paginated views // how many paginated views
let p = Math.ceil(n / LIMIT_PER_PAGE); setPages(Math.ceil(n / LIMIT_PER_PAGE));
setPages(p);
// divide events in parts of LIMIT_PER_PAGE // divide events in parts of LIMIT_PER_PAGE
const eventStart = LIMIT_PER_PAGE * selPage; const eventStart = LIMIT_PER_PAGE * selPage;
const eventEnd = LIMIT_PER_PAGE * (selPage + 1); const eventEnd = LIMIT_PER_PAGE * (selPage + 1);
let e = events.slice(eventStart, eventEnd); setPage(events.slice(eventStart, eventEnd));
setPage(e);
// if array is completely in past, show depending on SCROLL_PAST // if array is completely in past, show depending on SCROLL_PAST
}, [events, selPage, LIMIT_PER_PAGE]); }, [events, selPage, LIMIT_PER_PAGE]);
@@ -42,14 +62,10 @@ export default function Paginator(props) {
let selectedState = 0; let selectedState = 0;
return ( if (events?.length < 1) {
<> return <Empty text='No events to show' />;
<div className={style.nav}> } else {
{pages > 1 && return (
[...Array(pages)].map((p, i) => (
<div key={i} className={i === selPage ? style.navItemSelected : style.navItem} />
))}
</div>
<div className={style.entries}> <div className={style.entries}>
{page.map((e) => { {page.map((e) => {
if (e.id === selectedId) selectedState = 1; if (e.id === selectedId) selectedState = 1;
@@ -67,8 +83,8 @@ export default function Paginator(props) {
); );
})} })}
</div> </div>
</> );
); }
} }
Paginator.propTypes = { Paginator.propTypes = {
@@ -77,4 +93,6 @@ Paginator.propTypes = {
limit: PropTypes.number, limit: PropTypes.number,
time: PropTypes.number, time: PropTypes.number,
isBackstage: PropTypes.bool, 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 React from 'react';
import style from './TitleCard.module.css'; import style from './TitleCard.module.scss';
export default function TitleCard(props) { export default function TitleCard(props) {
const { label, title, subtitle, presenter } = props; const { label, title, subtitle, presenter } = props;
@@ -1,27 +1,24 @@
@use '../../../styles/main' as *;
.label { .label {
font-size: 1.3vw; @include card-label;
color: #ff7597;
} }
.title, .title,
.subtitle, .subtitle,
.presenter { .presenter {
white-space: nowrap; @include ellipsis;
overflow: hidden;
text-overflow: ellipsis;
} }
.title { .title {
color: #ddd; @include card-title;
font-size: 2.5vw; font-size: 2.5vw;
flex: 1; flex: 1;
font-weight: 600;
} }
.subtitle, .subtitle,
.presenter { .presenter {
color: #aaa; color: $subtitle-gray;
} }
.subtitle { .subtitle {
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import style from './TitleSide.module.css'; import style from './TitleSide.module.scss';
export default function TitleSide(props) { export default function TitleSide(props) {
const { type, label, title, subtitle, presenter } = props; const { type, label, title, subtitle, presenter } = props;
@@ -1,19 +1,19 @@
@use '../../../styles/main' as *;
.label { .label {
font-size: 1.3vw; @include card-label;
color: #ff7597;
} }
.nowTitle, .nowTitle,
.nextTitle { .nextTitle {
color: #ddd; @include card-title;
font-weight: 600;
} }
.nowSubtitle, .nowSubtitle,
.nowPresenter, .nowPresenter,
.nextSubtitle, .nextSubtitle,
.nextPresenter { .nextPresenter {
color: #aaa; color: $subtitle-gray;
} }
.nowTitle { .nowTitle {
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { stringFromMillis } from 'ontime-utils/time';
import style from './Paginator.module.css';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { stringFromMillis } from '../../utils/time';
import style from './Paginator.module.scss';
export default function TodayItem(props) { export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = 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; else if (selected === 2) selectStyle = style.entryFuture;
return ( return (
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}> <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}`} {`${start} · ${end}`}
</div> </div>
<div className={style.entryTitle}>{title}</div> <div className={style.entryTitle}>{title}</div>
@@ -29,7 +29,7 @@ export default function TodayItem(props) {
} }
TodayItem.propTypes = { TodayItem.propTypes = {
selected: PropTypes.bool, selected: PropTypes.number,
timeStart: PropTypes.number, timeStart: PropTypes.number,
timeEnd: PropTypes.number, timeEnd: PropTypes.number,
title: PropTypes.string, title: PropTypes.string,
@@ -15,7 +15,7 @@
} }
.preview { .preview {
color: #666; color: $bg-gray-500;
} }
.inline { .inline {
+22 -21
View File
@@ -1,35 +1,16 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { forgivingStringToMillis } from '../utils/dateConfig'; import { forgivingStringToMillis } from '../utils/dateConfig';
import { stringFromMillis } from 'ontime-utils/time';
import style from './EditableTimer.module.css';
import { LoggingContext } from '../../app/context/LoggingContext'; import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { stringFromMillis } from '../utils/time';
import style from './EditableTimer.module.scss';
export default function EditableTimer(props) { export default function EditableTimer(props) {
const { name, actionHandler, time, delay, validate, previousEnd } = props; const { name, actionHandler, time, delay, validate, previousEnd } = props;
const { emitError } = useContext(LoggingContext); const { emitError } = useContext(LoggingContext);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
// prepare time fields
const validateValue = (value) => {
const success = handleSubmit(value);
if (success) {
const ms = forgivingStringToMillis(value);
setValue(stringFromMillis(ms + delay));
} else {
setValue(stringFromMillis(time + delay));
}
};
useEffect(() => {
if (time == null) return;
try {
setValue(stringFromMillis(time + delay));
} catch (error) {
emitError(`Unable to parse date: ${error.text}`);
}
}, [time, delay, emitError]);
const handleSubmit = (value) => { const handleSubmit = (value) => {
// Check if there is anything there // Check if there is anything there
@@ -67,6 +48,26 @@ export default function EditableTimer(props) {
return true; 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; const isDelayed = delay != null && delay !== 0;
return ( return (
@@ -1,7 +1,9 @@
@use '../../styles/main'as *;
.editable, .editable,
.delayedEditable { .delayedEditable {
background-color: rgba(255, 255, 255, 0.03); background-color: $input-bg;
border: 1px solid rgba(255, 255, 255, 0.05); border: $input-border;
width: 6.5em; width: 6.5em;
letter-spacing: 1px; letter-spacing: 1px;
height: fit-content; height: fit-content;
@@ -11,5 +13,5 @@
} }
.delayedEditable { .delayedEditable {
border: 1px solid #d69e2e55; border: $input-delayed-border;
} }
+8 -3
View File
@@ -1,13 +1,18 @@
import React from 'react'; import React from 'react';
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg'; 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) { export default function Empty(props) {
const { text } = props; const { text, ...rest } = props;
return ( return (
<div className={style.emptyContainer}> <div className={style.emptyContainer} {...rest}>
<Emptyimage className={style.empty} /> <Emptyimage className={style.empty} />
<span className={style.text}>{text}</span> <span className={style.text}>{text}</span>
</div> </div>
); );
} }
Empty.propTypes = {
text: PropTypes.string,
}
@@ -1,3 +1,5 @@
@use '../../styles/main' as *;
.emptyContainer { .emptyContainer {
width: 100%; width: 100%;
text-align: center; text-align: center;
@@ -5,13 +7,11 @@
.empty { .empty {
width: 100%; width: 100%;
margin: auto 0;
margin-top: 10vh;
opacity: 0.3; opacity: 0.3;
} }
.text { .text {
color: rgba(0, 0, 0, 0.17); color: $bg-black-300;
font-weight: 600; font-weight: 600;
font-size: 2em; font-size: 2em;
} }
@@ -6,7 +6,7 @@ import {
millisToSeconds, millisToSeconds,
timeStringToMillis, timeStringToMillis,
} from '../dateConfig'; } from '../dateConfig';
import { stringFromMillis } from 'ontime-utils/time'; import { stringFromMillis } from '../time';
describe('test string from formatDisplay function', () => { describe('test string from formatDisplay function', () => {
it('test with null values', () => { it('test with null values', () => {
+2 -2
View File
@@ -21,7 +21,7 @@ export function formatDisplay(seconds, hideZero = false) {
const minutes = Math.floor((s % 3600) / 60); const minutes = Math.floor((s % 3600) / 60);
if (hideZero && hours < 1) return [minutes, s % 60].map(format).join(':'); 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 === 1) return Math.abs(time[0] * mts);
if (time.length === 2) return Math.abs(time[0]) * mtm + time[1] * 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; if (time.length === 3) return Math.abs(time[0]) * mth + time[1] * mtm + time[2] * mts;
else return 0; return 0;
}; };
/** /**
+3 -2
View File
@@ -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 * @description From a list of events, returns only events of type event with calculated delays
* @param {Object[]} events - given events * @param {Object[]} events - given events
* @returns {Object[]} Filtered events with calculated delays * @returns {Object[]} Filtered events with calculated delays
*/ */
export const getEventsWithDelay = (events) => { export const getEventsWithDelay = (events) => {
if (events == null) return []; if (events == null) return [];
@@ -66,7 +67,7 @@ export const formatEventList = (events, selectedId, nextId, showEnd = false) =>
const givenEvents = [...events]; const givenEvents = [...events];
// format list // format list
let formattedEvents = []; const formattedEvents = [];
for (const g of givenEvents) { for (const g of givenEvents) {
const start = stringFromMillis(g.timeStart, false); const start = stringFromMillis(g.timeStart, false);
const end = stringFromMillis(g.timeEnd, 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 { render, screen } from '@testing-library/react';
import SocketProvider from 'app/context/socketContext'; import SocketProvider from 'app/context/socketContext';
import MessageControl from '../MessageControl'; import MessageControl from '../message/MessageControl';
// need to inject the socket provider to make component // need to inject the socket provider to make component
// render without failing // render without failing
@@ -1,6 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import SocketProvider from 'app/context/socketContext'; import SocketProvider from 'app/context/socketContext';
import PlaybackControl from '../PlaybackControl'; import PlaybackControl from '../playback/PlaybackControl';
test('check that playback control renders', async () => { test('check that playback control renders', async () => {
// need to inject the socket provider to make component // 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>
</>
);
}
@@ -1,41 +1,9 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { useSocket } from 'app/context/socketContext'; import { useSocket } from 'app/context/socketContext';
import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn'; import InputRow from './InputRow';
import OnAirIconBtn from '../../common/components/buttons/OnAirIconBtn'; import OnAirIconBtn from '../../../common/components/buttons/OnAirIconBtn';
import style from './MessageControl.module.scss'; 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() { export default function MessageControl() {
const socket = useSocket(); const socket = useSocket();
const [pres, setPres] = useState({ const [pres, setPres] = useState({
@@ -1,5 +1,5 @@
@use '../../styles/main' as *; @use '../../../styles/main' as *;
@use '../../styles/mixins' as *; @use '../../../styles/mixins' as *;
.messageContainer, .messageContainer,
.onAirToggle { .onAirToggle {
@@ -22,13 +22,13 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
font-size: 0.9em; font-size: 0.9em;
color: #ccc; color: $label-gray;
} }
.inline { .inline {
border-radius: 4px; border-radius: 4px;
background-color: rgba(255, 255, 255, 0.05); background-color: $input-bg;
border: 1px solid rgba(255, 255, 255, 0.05); border: $input-border;
} }
.padleft { .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,
};
@@ -63,7 +63,7 @@ export default function PlaybackControl() {
}; };
}, [socket]); }, [socket]);
const playbackControl = async (action, payload) => { const playbackControl = async (action) => {
switch (action) { switch (action) {
case 'start': case 'start':
socket.emit('set-playstate', 'start'); socket.emit('set-playstate', 'start');
@@ -1,16 +1,16 @@
@use '../../../styles/main' as *;
@use '../../../styles/mixins' as *;
.mainContainer { .mainContainer {
width: 100%; width: 100%;
display: flex; display: grid;
margin: 0 auto; margin: 0 auto;
flex-direction: column;
gap: 5px; gap: 5px;
} }
.timeContainer, .timeContainer,
.playbackContainer { .playbackContainer {
background-color: rgba(0, 0, 0, 0.05); @include second-container;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 4px;
padding: 0.5em; padding: 0.5em;
} }
@@ -43,7 +43,7 @@
.indRoll, .indRoll,
.indDelay, .indDelay,
.indNegative { .indNegative {
background-color: rgba(0, 0, 0, 0.05); background-color: $bg-black-300;
} }
.indRoll, .indRoll,
@@ -56,7 +56,7 @@
} }
.indRollActive { .indRollActive {
background-color: #2b6cb0; background-color: $ontime-roll;
} }
.indNegative, .indNegative,
@@ -67,11 +67,11 @@
} }
.indNegativeActive { .indNegativeActive {
background-color: #ff7597; background-color: $ontime-pink;
} }
.indDelayActive { .indDelayActive {
background-color: #dd6b20; background-color: $ontime-delay;
} }
.btn { .btn {
@@ -102,17 +102,17 @@
} }
.time { .time {
color: #ccc; color: $header-gray;
font-size: 1.1em; font-size: 1.1em;
} }
.tag { .tag {
color: #aaa; color: $label-gray;
font-size: 0.9em; font-size: 0.9em;
} }
.rolltag { .rolltag {
color: #2b6cb0; color: $ontime-roll;
font-size: 0.9em; 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 React from 'react';
import { HStack } from '@chakra-ui/react';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical'; import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn'; import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
@@ -20,10 +21,10 @@ export default function BlockBlock(props) {
<span className={style.drag} {...provided.dragHandleProps}> <span className={style.drag} {...provided.dragHandleProps}>
<FiMoreVertical /> <FiMoreVertical />
</span> </span>
<div className={style.actionOverlay}> <HStack spacing='0.5em' className={style.actionOverlay}>
<DeleteIconBtn actionHandler={actionHandler} /> <DeleteIconBtn actionHandler={actionHandler} />
<ActionButtons showAdd showDelay actionHandler={actionHandler} /> <ActionButtons showAdd showDelay actionHandler={actionHandler} />
</div> </HStack>
</div> </div>
)} )}
</Draggable> </Draggable>
@@ -41,7 +41,6 @@
align-content: center; align-content: center;
align-self: flex-start; align-self: flex-start;
padding-top: 0.2em; padding-top: 0.2em;
gap: 0.5em;
opacity: 0.8; opacity: 0.8;
transition: linear 0.1s; transition: linear 0.1s;
justify-self: end; justify-self: end;
@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { HStack } from '@chakra-ui/react';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical'; import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import { millisToMinutes } from 'common/utils/dateConfig'; import { millisToMinutes } from 'common/utils/dateConfig';
@@ -16,7 +17,7 @@ export default function DelayBlock(props) {
eventsHandler('applyDelay', { id: data.id, duration: data.duration }); 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 ( return (
<Draggable key={data.id} draggableId={data.id} index={index}> <Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => ( {(provided) => (
@@ -25,11 +26,11 @@ export default function DelayBlock(props) {
<FiMoreVertical /> <FiMoreVertical />
</span> </span>
<DelayInput className={style.input} value={delayValue} actionHandler={actionHandler} /> <DelayInput className={style.input} value={delayValue} actionHandler={actionHandler} />
<div className={style.actionOverlay}> <HStack spacing='0.5em' className={style.actionOverlay}>
<ApplyIconBtn clickhandler={applyDelayHandler} /> <ApplyIconBtn clickhandler={applyDelayHandler} />
<DeleteIconBtn actionHandler={actionHandler} /> <DeleteIconBtn actionHandler={actionHandler} />
<ActionButtons showAdd actionHandler={actionHandler} /> <ActionButtons showAdd actionHandler={actionHandler} />
</div> </HStack>
</div> </div>
)} )}
</Draggable> </Draggable>
@@ -52,7 +52,6 @@
align-content: center; align-content: center;
align-self: flex-start; align-self: flex-start;
padding-top: 0.2em; padding-top: 0.2em;
gap: 0.5em;
opacity: 0.8; opacity: 0.8;
transition: linear 0.1s; transition: linear 0.1s;
} }
+3 -3
View File
@@ -11,8 +11,8 @@ import { CollapseProvider } from '../../app/context/CollapseContext';
import styles from './Editor.module.scss'; import styles from './Editor.module.scss';
const EventListWrapper = lazy(() => import('features/editors/list/EventListWrapper')); const EventListWrapper = lazy(() => import('features/editors/list/EventListWrapper'));
const PlaybackControl = lazy(() => import('features/control/PlaybackControl')); const PlaybackControl = lazy(() => import('features/control/playback/PlaybackControl'));
const MessageControl = lazy(() => import('features/control/MessageControl')); const MessageControl = lazy(() => import('features/control/message/MessageControl'));
const Info = lazy(() => import('features/info/Info')); const Info = lazy(() => import('features/info/Info'));
export default function Editor() { export default function Editor() {
@@ -35,7 +35,7 @@ export default function Editor() {
<CollapseProvider> <CollapseProvider>
<Box id='settings' className={styles.settings}> <Box id='settings' className={styles.settings}>
<ErrorBoundary> <ErrorBoundary>
<MenuBar onOpen={onOpen} isOpen={isOpen} /> <MenuBar onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
</ErrorBoundary> </ErrorBoundary>
</Box> </Box>
@@ -1,9 +1,11 @@
@use '../../styles/main' as *;
.mainContainer { .mainContainer {
background: linear-gradient(90deg, #202020 0%, #121212 100%); background: $bg-black;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: auto; margin: auto;
color: #fffd; color: $title-white;
padding: max(16px, 2vh); padding: max(16px, 2vh);
display: grid; display: grid;
@@ -94,14 +96,14 @@
h1 { h1 {
font-size: max(1.5em, 16px); font-size: max(1.5em, 16px);
color: rgba(255, 255, 255, 0.63); color: $bg-gray-100;
padding-bottom: 0.25em; padding-bottom: 0.25em;
} }
.mainContainer > div { .mainContainer > div {
border-radius: 0.5em; border-radius: 0.5em;
height: 100%; height: 100%;
background-color: rgba(255, 255, 255, 0.13); background-color: $bg-gray-1000;
padding: 0.8em 1.5em; padding: 0.8em 1.5em;
display: flex; 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 React, { useContext, useMemo } from 'react';
import Icon from '@chakra-ui/icon'; import Icon from '@chakra-ui/icon';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp'; 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 { 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 { millisToMinutes } from 'common/utils/dateConfig';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CollapseContext } from '../../../app/context/CollapseContext'; import { CollapseContext } from '../../../app/context/CollapseContext';
import style from './EventBlock.module.css'; import CollapsedBlock from './CollapsedBlock';
import ExpandedBlock from './ExpandedBlock';
const ExpandedBlock = (props) => { import style from './EventBlock.module.scss';
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,
};
export default function EventBlock(props) { export default function EventBlock(props) {
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler, next } = props; const { data, selected, delay, index, eventIndex, previousEnd, actionHandler, next } = props;
@@ -1,3 +1,5 @@
@use '../../../styles/main' as *;
/* ============= COMMON ============= */ /* ============= COMMON ============= */
.event { .event {
margin: 0.2em 0; margin: 0.2em 0;
@@ -8,11 +10,11 @@
border-radius: 8px; border-radius: 8px;
font-size: 15px; font-size: 15px;
border: 1px solid rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.05);
background-color: $bg-gray-950;
display: grid; display: grid;
gap: 0.5em; gap: 0.5em;
background-color: rgba(255, 255, 255, 0.02);
} }
.active { .active {
@@ -69,7 +71,7 @@
.nextDisabled { .nextDisabled {
width: max-content; width: max-content;
padding: 0 0.2em; padding: 0 0.2em;
color: #4bffab; color: $ontime-accent;
transition: 0.3s; transition: 0.3s;
} }
@@ -199,7 +201,6 @@
align-content: center; align-content: center;
align-self: flex-start; align-self: flex-start;
padding-top: 0.2em; padding-top: 0.2em;
gap: 0.5em;
opacity: 0.8; opacity: 0.8;
transition: linear 0.1s; 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]); }, [selectedId, isCursorLocked]);
if (events.length < 1) { if (events.length < 1) {
return <Empty text='No Events' />; return <Empty text='No Events' style={{marginTop: "10vh"}} />;
} }
// DND // DND
@@ -178,7 +178,7 @@ export default function EventList(props) {
)} )}
<div <div
ref={cursor === index ? cursorRef : undefined} ref={cursor === index ? cursorRef : undefined}
className={cursor === index ? style.cursor : undefined} className={cursor === index ? style.cursor : ''}
> >
<EventListItem <EventListItem
type={e.type} type={e.type}
@@ -27,7 +27,6 @@ const EventListItem = (props) => {
eventsHandler, eventsHandler,
delay, delay,
previousEnd, previousEnd,
...rest
} = props; } = props;
const { emitError } = useContext(LoggingContext); const { emitError } = useContext(LoggingContext);
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext); const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
@@ -42,6 +41,7 @@ const EventListItem = (props) => {
(start, end) => (start > end ? end + 86400000 - start : end - start), (start, end) => (start > end ? end + 86400000 - start : end - start),
[] []
); );
// Create / delete new events // Create / delete new events
const actionHandler = useCallback( const actionHandler = useCallback(
(action, payload) => { (action, payload) => {
@@ -101,7 +101,7 @@ const EventListItem = (props) => {
break; break;
} }
}, },
[data, defaultPublic, emitError, eventsHandler, index, starTimeIsLastEnd] [calculateDuration, data, defaultPublic, emitError, eventsHandler, index, starTimeIsLastEnd]
); );
switch (type) { switch (type) {
@@ -39,7 +39,7 @@ export default function EventListWrapper() {
} }
// optimistically update object, temp ID until refetch // optimistically update object, temp ID until refetch
let optimistic = [...previousEvents]; const optimistic = [...previousEvents];
optimistic.splice(newEvent.order, 0, { optimistic.splice(newEvent.order, 0, {
...newEvent, ...newEvent,
id: new Date().toISOString(), id: new Date().toISOString(),
@@ -128,8 +128,7 @@ export default function EventListWrapper() {
// Snapshot the previous value // Snapshot the previous value
const previousEvents = queryClient.getQueryData(EVENTS_TABLE); const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
let filtered = [...previousEvents]; const filtered = [...previousEvents].filter((e) => e.id === 'eventId')
filtered.filter((e) => e.id === 'eventId');
// optimistically update object // optimistically update object
queryClient.setQueryData(EVENTS_TABLE, filtered); queryClient.setQueryData(EVENTS_TABLE, filtered);
@@ -158,7 +157,7 @@ export default function EventListWrapper() {
// Snapshot the previous value // Snapshot the previous value
const previousEvents = queryClient.getQueryData(EVENTS_TABLE); const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
let clear = []; const clear = [];
// optimistically update object // optimistically update object
queryClient.setQueryData(EVENTS_TABLE, clear); queryClient.setQueryData(EVENTS_TABLE, clear);
@@ -225,13 +224,13 @@ export default function EventListWrapper() {
// Events API // Events API
const eventsHandler = useCallback( const eventsHandler = useCallback(
async (action, payload, options = undefined) => { async (action, payload, options) => {
switch (action) { switch (action) {
case 'add': case 'add':
try { try {
let newEvent = { ...payload }; const newEvent = { ...payload };
// there is an option to pass an index of an array to use as start time // 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; newEvent.timeStart = data[options.startIsLastEnd].timeEnd || 0;
} }
// hard coding duration value to be as expected for now // hard coding duration value to be as expected for now
@@ -1,10 +1,11 @@
@use '../../../styles/main' as *;
@use '../../../styles/mixins' as *;
.eventContainer { .eventContainer {
@include second-container;
margin-top: 1em; margin-top: 1em;
display: flex; display: flex;
flex-direction: column; 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; padding: 8px;
overflow-y: scroll; overflow-y: scroll;
height: 100%; height: 100%;
@@ -21,14 +22,6 @@
} }
.cursor { .cursor {
width: 100%; box-shadow: 2px 2px 0 $ontime-pink;
background: linear-gradient(
180deg,
#ff7597 2%,
#0001 3%,
#0001 97%,
#ff7597 98%
);
border-radius: 14px; border-radius: 14px;
} }
+6 -24
View File
@@ -3,11 +3,7 @@
@mixin container { @mixin container {
margin-top: 1em; margin-top: 1em;
display: flex; @include second-container;
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; padding: 8px;
} }
@@ -73,7 +69,7 @@
.emptyLabel { .emptyLabel {
font-size: 0.8em; font-size: 0.8em;
color: #555; color: $bg-gray-700;
} }
.notes { .notes {
@@ -85,26 +81,12 @@
.if { .if {
font-size: 0.8em; font-size: 0.8em;
color: $ontime-accent; color: $ontime-accent;
@include container-bg; background-color: $bg-gray-900;
padding: 0 0.5em;
margin: 0 0.5em;
} }
ul > li { ul > li {
font-size: 0.9em; font-size: 0.9em;
color: #fff; color: $text-white;
}
.moreExpanded,
.moreCollapsed {
cursor: pointer;
color: #fff;
}
.moreExpanded {
transform: scaleY(-1);
transition: transform 0.3s;
}
.moreCollapsed {
transform: scaleY(1);
transition: transform 0.3s;
} }
+38 -22
View File
@@ -1,7 +1,8 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import style from './InfoLogger.module.scss'; import { HStack } from '@chakra-ui/react';
import CollapseBar from "../../common/components/collapseBar/CollapseBar"; import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import { LoggingContext } from '../../app/context/LoggingContext'; import { LoggingContext } from '../../app/context/LoggingContext';
import style from './InfoLogger.module.scss';
export default function InfoLogger() { export default function InfoLogger() {
const { logData, clearLog } = useContext(LoggingContext); const { logData, clearLog } = useContext(LoggingContext);
@@ -16,6 +17,10 @@ export default function InfoLogger() {
const [showUser, setShowUser] = useState(true); const [showUser, setShowUser] = useState(true);
useEffect(() => { useEffect(() => {
if (!logData) {
return;
}
const matchers = []; const matchers = [];
if (showUser) { if (showUser) {
matchers.push('USER'); matchers.push('USER');
@@ -36,12 +41,10 @@ export default function InfoLogger() {
matchers.push('PLAYBACK'); matchers.push('PLAYBACK');
} }
const d = logData.filter((d) => ( const d = logData.filter((d) => matchers.some((m) => d.origin === m));
matchers.some((m) => d.origin === m)
))
setData(d); setData(d);
},[logData, showUser, showClient, showServer, showPlayback, showRx, showTx]) }, [logData, showUser, showClient, showServer, showPlayback, showRx, showTx]);
const disableOthers = (toEnable) => { const disableOthers = (toEnable) => {
toEnable === 'USER' ? setShowUser(true) : setShowUser(false); toEnable === 'USER' ? setShowUser(true) : setShowUser(false);
@@ -50,62 +53,75 @@ export default function InfoLogger() {
toEnable === 'RX' ? setShowRx(true) : setShowRx(false); toEnable === 'RX' ? setShowRx(true) : setShowRx(false);
toEnable === 'TX' ? setShowTx(true) : setShowTx(false); toEnable === 'TX' ? setShowTx(true) : setShowTx(false);
toEnable === 'PLAYBACK' ? setShowPlayback(true) : setShowPlayback(false); toEnable === 'PLAYBACK' ? setShowPlayback(true) : setShowPlayback(false);
} };
return ( return (
<div className={collapsed ? style.container : style.container__expanded}> <div className={collapsed ? style.container : style.container__expanded}>
<CollapseBar title='Log' isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)} /> <CollapseBar title='Log' isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)} />
{!collapsed && ( {!collapsed && (
<> <>
<div className={style.toggleBar}> <HStack className={style.toggleBar}>
<div <div
onClick={() => setShowUser((s) => !s)} onClick={() => setShowUser((s) => !s)}
onAuxClick={() => disableOthers('USER')} onAuxClick={() => disableOthers('USER')}
className={(showUser) ? style.active : null}> className={showUser ? style.active : null}
>
USER USER
</div> </div>
<div <div
onClick={() => setShowClient((s) => !s)} onClick={() => setShowClient((s) => !s)}
onAuxClick={() => disableOthers('CLIENT')} onAuxClick={() => disableOthers('CLIENT')}
className={(showClient) ? style.active : null}> className={showClient ? style.active : null}
>
CLIENT CLIENT
</div> </div>
<div <div
onClick={() => setShowServer((s) => !s)} onClick={() => setShowServer((s) => !s)}
onAuxClick={() => disableOthers('SERVER')} onAuxClick={() => disableOthers('SERVER')}
className={(showServer) ? style.active : null}> className={showServer ? style.active : null}
>
SERVER SERVER
</div> </div>
<div <div
onClick={() => setShowPlayback((s) => !s)} onClick={() => setShowPlayback((s) => !s)}
onAuxClick={() => disableOthers('PLAYBACK')} onAuxClick={() => disableOthers('PLAYBACK')}
className={(showPlayback) ? style.active : null}> className={showPlayback ? style.active : null}
>
Playback Playback
</div> </div>
<div <div
onClick={() => setShowRx((s) => !s)} onClick={() => setShowRx((s) => !s)}
onAuxClick={() => disableOthers('RX')} onAuxClick={() => disableOthers('RX')}
className={(showRx) ? style.active : null}> className={showRx ? style.active : null}
>
RX RX
</div> </div>
<div <div
onClick={() => setShowTx((s) => !s)} onClick={() => setShowTx((s) => !s)}
onAuxClick={() => disableOthers('TX')} onAuxClick={() => disableOthers('TX')}
className={(showTx) ? style.active : null}> className={showTx ? style.active : null}
>
TX TX
</div> </div>
<div <div onClick={clearLog} className={style.clear}>
onClick={clearLog}
className={style.clear}>
Clear Clear
</div> </div>
</div> </HStack>
<ul className={style.log}> <ul className={style.log}>
{data.map((d) => ( {data.map((d) => (
<li key={d.id} className={d.level === 'INFO' ? style.info : d.level === 'WARN' ? style.warn : d.level === 'ERROR' ? style.error : ''}> <li
<div key={d.id}
className={style.time} className={
>{d.time}</div> 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.origin}>{d.origin}</div>
<div className={style.msg}>{d.text}</div> <div className={style.msg}>{d.text}</div>
</li> </li>
+13 -15
View File
@@ -17,8 +17,11 @@
height: 100%; height: 100%;
overflow-y: scroll; overflow-y: scroll;
font-size: 0.8em; font-size: 0.8em;
user-select:text; user-select: text;
@include container-bg; @include third-container;
padding: 0 0.5em;
margin: 0 0.5em;
li { li {
display: flex; display: flex;
@@ -36,39 +39,33 @@
} }
li.info { li.info {
color: #aaa; color: $info-gray;
} }
li.warn { li.warn {
color: #dd6b20; color: $warning-orange;
} }
li.error { li.error {
color: #f00; color: $error-red;
} }
.entry:hover { li:hover {
color: #ddd; color: $info-gray-hover;
} }
} }
.info { .info {
color: #fff; color: $text-white;
} }
.error { .error {
color: red; color: red;
} }
.client {
color: lightblue;
}
.toggleBar { .toggleBar {
display: flex; display: flex;
font-size: 0.7em; font-size: 0.7em;
justify-content: flex-start; justify-content: flex-start;
gap: 1em; padding: 0.5em 8px;
padding: 0.5em 0;
font-weight: 600; font-weight: 600;
div { div {
@@ -85,6 +82,7 @@
} }
.clear { .clear {
margin-left: auto;
border: 1px solid rgba($ontime-pink, 0.5); border: 1px solid rgba($ontime-pink, 0.5);
} }
} }
+2 -3
View File
@@ -20,10 +20,9 @@ export default function InfoNif() {
isCollapsed={collapsed} isCollapsed={collapsed}
onClick={() => setCollapsed((c) => !c)} onClick={() => setCollapsed((c) => !c)}
/> />
{!collapsed && ( {!collapsed && (status === 'success') &&(
<div> <div>
{status === 'success' && {data?.networkInterfaces.map((e) => (
data?.networkInterfaces.map((e) => (
<a <a
key={e.address} key={e.address}
href='#!' href='#!'
+11 -26
View File
@@ -1,7 +1,6 @@
import React, { useState } from 'react'; 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 style from './Info.module.scss';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
export default function InfoTitle(props) { export default function InfoTitle(props) {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
@@ -15,42 +14,28 @@ export default function InfoTitle(props) {
return ( return (
<div className={style.container}> <div className={style.container}>
<div className={roll ? style.headerRoll : style.header}> <CollapseBar
{title} title={title}
{collapsed && ( isCollapsed={collapsed}
<span className={style.collapsedTitle}>{data.title}</span> onClick={() => setCollapsed((c) => !c)}
)} roll={roll}
<Icon />
className={collapsed ? style.moreCollapsed : style.moreExpanded}
as={FiChevronUp}
onClick={() => setCollapsed((c) => !c)}
/>
</div>
{!collapsed && ( {!collapsed && (
<> <>
<div className={style.labelContainer}> <div className={style.labelContainer}>
<span className={noTitl ? style.emptyLabel : style.label}> <span className={noTitl ? style.emptyLabel : style.label}>Title</span>
Title
</span>
{data.title} {data.title}
</div> </div>
<div className={style.labelContainer}> <div className={style.labelContainer}>
<span className={noPres ? style.emptyLabel : style.label}> <span className={noPres ? style.emptyLabel : style.label}>Presenter</span>
Presenter
</span>
{data.presenter} {data.presenter}
</div> </div>
<div className={style.labelContainer}> <div className={style.labelContainer}>
<span className={noSubt ? style.emptyLabel : style.label}> <span className={noSubt ? style.emptyLabel : style.label}>Subtitle</span>
Subtitle
</span>
{data.subtitle} {data.subtitle}
</div> </div>
<div className={style.notes}> <div className={style.notes}>
<span className={noNote ? style.emptyLabel : style.label}> <span className={noNote ? style.emptyLabel : style.label}>Note</span>
Note
</span>
{data.note} {data.note}
</div> </div>
</> </>
+17 -12
View File
@@ -1,5 +1,5 @@
import React, { memo, useContext } from 'react'; 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 { CursorContext } from '../../app/context/CursorContext';
import MenuActionButtons from './MenuActionButtons'; import MenuActionButtons from './MenuActionButtons';
import CollapseBtn from 'common/components/buttons/CollapseBtn'; import CollapseBtn from 'common/components/buttons/CollapseBtn';
@@ -42,20 +42,25 @@ const EventListMenu = ({ eventsHandler }) => {
}; };
return ( return (
<div className={style.headerButtons}> <HStack className={style.headerButtons}>
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} /> <ButtonGroup isAttached>
<CollapseBtn size='sm' clickhandler={() => eventsHandler('collapseall')} /> <ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
<CollapseBtn size='sm' clickhandler={() => eventsHandler('collapseall')} />
</ButtonGroup>
<Divider orientation='vertical' /> <Divider orientation='vertical' />
<CursorUpBtn size='sm' clickhandler={() => actionHandler('cursorUp')} /> <ButtonGroup isAttached>
<CursorDownBtn size='sm' clickhandler={() => actionHandler('cursorDown')} /> <CursorUpBtn size='sm' clickhandler={() => actionHandler('cursorUp')} />
<CursorLockedBtn <CursorDownBtn size='sm' clickhandler={() => actionHandler('cursorDown')} />
size='sm' <CursorLockedBtn
clickhandler={() => actionHandler('togglelock')} size='sm'
active={isCursorLocked} clickhandler={() => actionHandler('togglelock')}
/> active={isCursorLocked}
width='3em'
/>
</ButtonGroup>
<Divider orientation='vertical' /> <Divider orientation='vertical' />
<MenuActionButtons actionHandler={actionHandler} size='sm' /> <MenuActionButtons actionHandler={actionHandler} size='sm' />
</div> </HStack>
); );
}; };
@@ -1,11 +1,4 @@
.headerButtons { .headerButtons {
display: flex;
gap: 0.5em;
align-content: center; align-content: center;
justify-content: flex-end; justify-content: flex-end;
} }
.menu {
color: #000;
background-color: rgba(255, 255, 255, 0.67);
}
+47 -36
View File
@@ -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 { useMutation, useQueryClient } from 'react-query';
import { downloadEvents, uploadEvents } from 'app/api/ontimeApi'; import { downloadEvents, uploadEvents } from 'app/api/ontimeApi';
import { EVENTS_TABLE } from 'app/api/apiConstants'; import { EVENTS_TABLE } from 'app/api/apiConstants';
@@ -12,9 +12,10 @@ import HelpIconBtn from './buttons/HelpIconBtn';
import UploadIconBtn from './buttons/UploadIconBtn'; import UploadIconBtn from './buttons/UploadIconBtn';
import { LoggingContext } from '../../app/context/LoggingContext'; import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { VStack } from '@chakra-ui/react';
export default function MenuBar(props) { export default function MenuBar(props) {
const { isOpen, onOpen } = props; const { isOpen, onOpen, onClose } = props;
const { emitError } = useContext(LoggingContext); const { emitError } = useContext(LoggingContext);
const hiddenFileInput = useRef(null); const hiddenFileInput = useRef(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -35,7 +36,7 @@ export default function MenuBar(props) {
}; };
const buttonStyle = { const buttonStyle = {
fontSize: '1.5em' fontSize: '1.5em',
}; };
const handleUpload = (event) => { const handleUpload = (event) => {
@@ -44,7 +45,7 @@ export default function MenuBar(props) {
// Limit file size to 1MB // Limit file size to 1MB
if (fileUploaded.size > 1000000) { if (fileUploaded.size > 1000000) {
emitError('Error: File size limit (1MB) exceeded') emitError('Error: File size limit (1MB) exceeded');
return; return;
} }
@@ -53,10 +54,10 @@ export default function MenuBar(props) {
try { try {
uploaddb.mutate(fileUploaded); uploaddb.mutate(fileUploaded);
} catch (error) { } catch (error) {
emitError(`Failed uploading file: ${error}`) emitError(`Failed uploading file: ${error}`);
} }
} else { } else {
emitError('Error: File type unknown') emitError('Error: File type unknown');
} }
// reset input value // reset input value
@@ -65,7 +66,7 @@ export default function MenuBar(props) {
const handleIPC = (action) => { const handleIPC = (action) => {
// Stop crashes when testing locally // Stop crashes when testing locally
if (window.process?.type === undefined) { if (typeof window.process?.type === 'undefined') {
if (action === 'help') { if (action === 'help') {
window.open('https://cpvalente.gitbook.io/ontime/'); 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 ( return (
<> <VStack>
<QuitIconBtn size='lg' clickhandler={() => handleIPC('shutdown')} /> <QuitIconBtn size='lg' clickhandler={() => handleIPC('shutdown')} />
<MaxIconBtn <MaxIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('max')} />
style={{ ...buttonStyle }} <MinIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('min')} />
size='lg'
clickhandler={() => handleIPC('max')}
/>
<MinIconBtn
style={{ ...buttonStyle }}
size='lg'
clickhandler={() => handleIPC('min')}
/>
<div className={style.gap} /> <div className={style.gap} />
<HelpIconBtn <HelpIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={() => handleIPC('help')} />
style={{ ...buttonStyle }}
size='lg'
clickhandler={() => handleIPC('help')}
/>
<SettingsIconBtn <SettingsIconBtn
style={{...buttonStyle}} style={{ ...buttonStyle }}
size='lg' size='lg'
className={isOpen ? style.open : ''} className={isOpen ? style.open : ''}
clickhandler={onOpen} clickhandler={onOpen}
@@ -126,22 +145,14 @@ export default function MenuBar(props) {
onChange={handleUpload} onChange={handleUpload}
accept='.json, .xlsx' accept='.json, .xlsx'
/> />
<UploadIconBtn <UploadIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={handleClick} />
style={{ ...buttonStyle }} <DownloadIconBtn style={{ ...buttonStyle }} size='lg' clickhandler={handleDownload} />
size='lg' </VStack>
clickhandler={handleClick}
/>
<DownloadIconBtn
style={{ ...buttonStyle }}
size='lg'
clickhandler={handleDownload}
/>
</>
); );
} }
MenuBar.propTypes = { MenuBar.propTypes = {
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
onOpen: PropTypes.func.isRequired, onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
}; };
@@ -1,21 +1,23 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import MenuBar from "../MenuBar"; import MenuBar from '../MenuBar';
import {queryClientMock} from "../../../__mocks__/QueryClient.mock"; import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
import {QueryClientProvider} from "react-query"; import { QueryClientProvider } from 'react-query';
const onOpenHandler = jest.fn(); const onOpenHandler = jest.fn();
const onCloseHandler = jest.fn();
const renderInMock = () => { const renderInMock = () => {
render( render(
<QueryClientProvider client={queryClientMock}> <QueryClientProvider client={queryClientMock}>
<MenuBar onOpen={onOpenHandler} /> <MenuBar onOpen={onOpenHandler} onClose={onCloseHandler} />
</QueryClientProvider> </QueryClientProvider>
) );
}; };
test('check that menu bar renders correctly', () => { test('check that menu bar renders correctly', () => {
// need to inject the react query provider // need to inject the react query provider
renderInMock(); renderInMock();
const nButtons = screen.getAllByRole("button").length; const nButtons = screen.getAllByRole('button').length;
expect(nButtons).toBe(7); expect(nButtons).toBe(7);
}); });
@@ -1,21 +1,23 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import MenuBar from "../MenuBar"; import MenuBar from '../MenuBar';
import {queryClientMock} from "../../../__mocks__/QueryClient.mock"; import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
import {QueryClientProvider} from "react-query"; import { QueryClientProvider } from 'react-query';
const onOpenHandler = jest.fn(); const onOpenHandler = jest.fn();
const onCloseHandler = jest.fn();
const renderInMock = () => { const renderInMock = () => {
render( render(
<QueryClientProvider client={queryClientMock}> <QueryClientProvider client={queryClientMock}>
<MenuBar onOpen={onOpenHandler} /> <MenuBar onOpen={onOpenHandler} onClose={onCloseHandler} />
</QueryClientProvider> </QueryClientProvider>
) );
}; };
test('check that menu bar renders correctly', () => { test('check that menu bar renders correctly', () => {
// need to inject the react query provider // need to inject the react query provider
renderInMock(); renderInMock();
const nButtons = screen.getAllByRole("button").length; const nButtons = screen.getAllByRole('button').length;
expect(nButtons).toBe(7); expect(nButtons).toBe(7);
}); });
@@ -78,7 +78,7 @@ export default function AppSettingsModal() {
// we might not have changed this // we might not have changed this
if (f.pinCode !== data.pinCode) { if (f.pinCode !== data.pinCode) {
let e = { status: false, message: '' }; const e = { status: false, message: '' };
// Validate fields // Validate fields
if (f.pinCode === '' || f.pinCode == null) { if (f.pinCode === '' || f.pinCode == null) {
@@ -45,13 +45,11 @@ export default function IntegrationSettingsModal() {
// set fields with error // set fields with error
if (e.status) { if (e.status) {
emitError(`Invalid Input: ${e.message}`); emitError(`Invalid Input: ${e.message}`);
return;
} else { } else {
await postInfo(f); await postInfo(f);
setChanged(false); setChanged(false);
setSubmitting(false);
} }
setSubmitting(false);
}; };
/** /**
@@ -97,7 +97,7 @@ export default function OscSettingsModal() {
setSubmitting(true); setSubmitting(true);
const f = formData; const f = formData;
let e = { status: false, message: '' }; const e = { status: false, message: '' };
// Validate fields // Validate fields
if (f.port < 1024 || f.port > 65535) { if (f.port < 1024 || f.port > 65535) {
@@ -137,7 +137,7 @@ export default function OscSettingsModal() {
/** /**
* Handles change of input field in local state * Handles change of input field in local state
* @param {string} field - object parameter to update * @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 handleChange = (field, value) => {
const temp = { ...formData }; const temp = { ...formData };
@@ -34,7 +34,7 @@ export default function TableOptionsModal() {
// validation step makes clean string // validation step makes clean string
const validatedFields = { ...userFields }; const validatedFields = { ...userFields };
let errors = false; const errors = false;
for (const field in validatedFields) { for (const field in validatedFields) {
validatedFields[field] = validatedFields[field].trim(); validatedFields[field] = validatedFields[field].trim();
} }
+1 -1
View File
@@ -7,10 +7,10 @@ import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
import { IoMoon } from '@react-icons/all-files/io5/IoMoon'; import { IoMoon } from '@react-icons/all-files/io5/IoMoon';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget'; import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
import { useSocket } from '../../app/context/socketContext'; import { useSocket } from '../../app/context/socketContext';
import { stringFromMillis } from 'ontime-utils/time';
import { formatDisplay } from '../../common/utils/dateConfig'; import { formatDisplay } from '../../common/utils/dateConfig';
import { Tooltip } from '@chakra-ui/tooltip'; import { Tooltip } from '@chakra-ui/tooltip';
import PlaybackIcon from './tableElements/PlaybackIcon'; import PlaybackIcon from './tableElements/PlaybackIcon';
import { stringFromMillis } from '../../common/utils/time';
import style from './Table.module.scss'; import style from './Table.module.scss';
export default function TableHeader() { export default function TableHeader() {
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck'; import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { stringFromMillis } from 'ontime-utils/time';
import EditableCell from './tableElements/EditableCell'; import EditableCell from './tableElements/EditableCell';
import { stringFromMillis } from '../../common/utils/time.js';
/** /**
* React - Table column object * React - Table column object
+1 -1
View File
@@ -3,9 +3,9 @@ import React, { useEffect, useState } from 'react';
import { fetchAllEvents } from 'app/api/eventsApi'; import { fetchAllEvents } from 'app/api/eventsApi';
import { fetchEvent } from 'app/api/eventApi'; import { fetchEvent } from 'app/api/eventApi';
import { useSocket } from 'app/context/socketContext'; import { useSocket } from 'app/context/socketContext';
import { stringFromMillis } from 'ontime-utils/time';
import { useFetch } from 'app/hooks/useFetch'; import { useFetch } from 'app/hooks/useFetch';
import { EVENT_TABLE, EVENTS_TABLE } from 'app/api/apiConstants'; import { EVENT_TABLE, EVENTS_TABLE } from 'app/api/apiConstants';
import { stringFromMillis } from '../../common/utils/time';
const withSocket = (Component) => { const withSocket = (Component) => {
return (props) => { return (props) => {
@@ -1,16 +1,19 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import QRCode from 'react-qr-code'; import QRCode from 'react-qr-code';
import { formatDisplay } from 'common/utils/dateConfig'; import { formatDisplay } from 'common/utils/dateConfig';
import style from './StageManager.module.css';
import Paginator from 'common/components/views/Paginator'; import Paginator from 'common/components/views/Paginator';
import NavLogo from 'common/components/nav/NavLogo'; import NavLogo from 'common/components/nav/NavLogo';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import TitleSide from 'common/components/views/TitleSide'; 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) { export default function StageManager(props) {
const { publ, title, time, backstageEvents, selectedId, general } = props; const { publ, title, time, backstageEvents, selectedId, general } = props;
const [filteredEvents, setFilteredEvents] = useState(null); const [filteredEvents, setFilteredEvents] = useState(null);
const [pageNumber, setPageNumber] = useState(0);
const [currentPage, setCurrentPage] = useState(0);
// Set window title // Set window title
useEffect(() => { useEffect(() => {
@@ -20,11 +23,9 @@ export default function StageManager(props) {
// calculate delays if any // calculate delays if any
useEffect(() => { useEffect(() => {
if (backstageEvents == null) return; if (backstageEvents == null) return;
const f = getEventsWithDelay(backstageEvents);
const f = getEventsWithDelay(backstageEvents)
setFilteredEvents(f); setFilteredEvents(f);
}, [backstageEvents]); }, [backstageEvents]);
// Format messages // Format messages
const showPubl = publ.text !== '' && publ.visible; const showPubl = publ.text !== '' && publ.visible;
@@ -37,22 +38,6 @@ export default function StageManager(props) {
if (time.isNegative) stageTimer = `-${stageTimer}`; if (time.isNegative) stageTimer = `-${stageTimer}`;
} }
// motion
const titleVariants = {
hidden: {
x: -1500,
},
visible: {
x: 0,
transition: {
duration: 1,
},
},
exit: {
x: -1500,
},
};
return ( return (
<div className={style.container__gray}> <div className={style.container__gray}>
<NavLogo /> <NavLogo />
@@ -102,17 +87,28 @@ export default function StageManager(props) {
</AnimatePresence> </AnimatePresence>
<div className={style.todayContainer}> <div className={style.todayContainer}>
<div className={style.label}>Today</div> <div className={style.todayHeaderBlock}>
<div className={style.entriesContainer}> <div className={style.label}>Today</div>
<Paginator selectedId={selectedId} events={filteredEvents} isBackstage /> <div className={style.nav}>
{pageNumber > 1 &&
[...Array(pageNumber).keys()].map((i) => (
<div
key={i}
className={i === currentPage ? style.navItemSelected : style.navItem}
/>
))}
</div>
</div> </div>
<Paginator
selectedId={selectedId}
events={filteredEvents}
isBackstage
setCurrentPage={setCurrentPage}
setPageNumber={setPageNumber}
/>
</div> </div>
<div <div className={showPubl ? style.publicContainer : style.publicContainerHidden}>
className={
showPubl ? style.publicContainer : style.publicContainerHidden
}
>
<div className={style.label}>Public message</div> <div className={style.label}>Public message</div>
<div className={style.message}>{publ.text}</div> <div className={style.message}>{publ.text}</div>
</div> </div>
@@ -134,11 +130,7 @@ export default function StageManager(props) {
</div> </div>
<div className={style.qr}> <div className={style.qr}>
{general.url != null && general.url !== '' && ( {general.url != null && general.url !== '' && (
<QRCode <QRCode value={general.url} size={window.innerWidth / 12} level='L' />
value={general.url}
size={window.innerWidth / 12}
level='L'
/>
)} )}
</div> </div>
</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