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

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