* 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
+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', () => {
@@ -0,0 +1,28 @@
import { generateId } from '../generate_id.js';
test('generate a valid 5 digit id', () => {
const id = generateId();
expect(id.length).toBe(5);
});
test('generate 100 with less than 110 attempts', () => {
const ids = new Set();
let attempts = 1;
while (ids.size < 100) {
ids.add(generateId());
attempts++;
}
expect(attempts).toBeLessThan(105);
});
describe('generate 1000 with less than 1020 attempts', () => {
const ids = new Set();
let attempts = 1;
while (ids.size < 1000) {
ids.add(generateId());
attempts++;
}
expect(attempts).toBeLessThan(1020);
});
+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);
+4
View File
@@ -0,0 +1,4 @@
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('1234567890abcdef', 5);
export const generateId = () => nanoid();
+67
View File
@@ -0,0 +1,67 @@
const mts = 1000; // millis to seconds
const mtm = 1000 * 60; // millis to minutes
const mth = 1000 * 60 * 60; // millis to hours
/**
* Returns current time in milliseconds
* @returns {number}
*/
export const nowInMillis = () => {
const now = new Date();
// extract milliseconds since midnight
let elapsed = now.getHours() * 3600000;
elapsed += now.getMinutes() * 60000;
elapsed += now.getSeconds() * 1000;
elapsed += now.getMilliseconds();
return elapsed;
};
/**
* @description Converts milliseconds to string representing time
* @param {number} ms - time in milliseconds
* @param {boolean} showSeconds - weather to show the seconds
* @param {string} delim - character between HH MM SS
* @param {string} ifNull - what to return if value is null
* @returns {string} String representing time 00:12:02
*/
export const stringFromMillis = (
ms,
showSeconds = true,
delim = ':',
ifNull = '...'
) => {
if (ms == null || isNaN(ms)) return ifNull;
const isNegative = ms < 0 ? '-' : '';
const millis = Math.abs(ms);
const showWith0 = (value) => (value < 10 ? `0${value}` : value);
const hours = showWith0(Math.floor(((millis / mth) % 60) % 24));
const minutes = showWith0(Math.floor((millis / mtm) % 60));
const seconds = showWith0(Math.floor((millis / mts) % 60));
return showSeconds
? `${isNegative}${
parseInt(hours) ? `${hours}${delim}` : `00${delim}`
}${minutes}${delim}${seconds}`
: `${isNegative}${parseInt(hours) ? `${hours}` : '00'}${delim}${minutes}`;
};
/**
* @description Converts an excel date to milliseconds
* @argument {string} excelDate - excel string date
* @returns {number} - time in milliseconds
*/
export const excelDateStringToMillis = (excelDate) => {
const date = new Date(excelDate);
if (date instanceof Date && !isNaN(date)) {
const h = date.getHours();
const m = date.getMinutes();
const s = date.getSeconds();
return h * mth + m * mtm + s * mts;
}
return 0;
};
@@ -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;
}
@@ -0,0 +1,15 @@
// used in both sm and public views
export const titleVariants = {
hidden: {
x: -1500,
},
visible: {
x: 0,
transition: {
duration: 1,
},
},
exit: {
x: -1500,
},
};
+24 -19
View File
@@ -1,13 +1,16 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import QRCode from 'react-qr-code';
import style from './Public.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 { titleVariants } from '../common/animation';
import style from './Public.module.scss';
export default function Public(props) {
const { publ, publicTitle, time, events, publicSelectedId, general } = props;
const [pageNumber, setPageNumber] = useState(0);
const [currentPage, setCurrentPage] = useState(0);
// Set window title
useEffect(() => {
@@ -18,20 +21,7 @@ export default function Public(props) {
const showPubl = publ.text !== '' && publ.visible;
// motion
const titleVariants = {
hidden: {
x: -1500,
},
visible: {
x: 0,
transition: {
duration: 1,
},
},
exit: {
x: -1500,
},
};
return (
<div className={style.container__gray}>
<NavLogo />
@@ -81,10 +71,25 @@ export default function Public(props) {
</AnimatePresence>
<div className={style.todayContainer}>
<div className={style.label}>Today</div>
<div className={style.entriesContainer}>
<Paginator selectedId={publicSelectedId} events={events} />
<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={publicSelectedId}
events={events}
isBackstage
setCurrentPage={setCurrentPage}
setPageNumber={setPageNumber}
/>
</div>
<div
@@ -0,0 +1,54 @@
@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 .... . 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: 1vw;
}
+25 -13
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import QRCode from 'react-qr-code';
import style from './Pip.module.css';
import style from './Pip.module.scss';
import Paginator from 'common/components/views/Paginator';
import NavLogo from 'common/components/nav/NavLogo';
import { AnimatePresence, motion } from 'framer-motion';
@@ -12,6 +12,8 @@ export default function Pip(props) {
const [size, setSize] = useState('');
const ref = useRef(null);
const [filteredEvents, setFilteredEvents] = useState(null);
const [pageNumber, setPageNumber] = useState(0);
const [currentPage, setCurrentPage] = useState(0);
// calculcate pip size
useLayoutEffect(() => {
@@ -29,7 +31,7 @@ export default function Pip(props) {
useEffect(() => {
if (backstageEvents == null) return;
let events = [...backstageEvents];
const events = [...backstageEvents];
// Add running delay
let delay = 0;
@@ -43,9 +45,7 @@ export default function Pip(props) {
}
// filter just events
let filtered = events.filter((e) => e.type === 'event');
setFilteredEvents(filtered);
setFilteredEvents(events.filter((e) => e.type === 'event'));
}, [backstageEvents]);
// Format messages
@@ -61,15 +61,27 @@ export default function Pip(props) {
<div className={style.eventTitle}>{general.title}</div>
<div className={style.todayContainer}>
<div className={style.label}>Today</div>
<div className={style.entriesContainer}>
<Paginator
selectedId={selectedId}
events={filteredEvents}
limit={15}
time={20}
/>
<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
limit={14}
time={20}
setCurrentPage={setCurrentPage}
setPageNumber={setPageNumber}
/>
</div>
<div className={style.pip} ref={ref}>
@@ -1,136 +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: 20vw 20vw 18vw 3vw 1fr;
grid-template-rows: 60vh 1fr 1fr 1fr;
grid-template-areas:
' pip pip pip . schd'
' titl titl titl . schd'
' info info clck . schd'
' info info time . schd';
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;
}
.pip {
grid-area: pip;
background-color: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 2555, 0.07);
width: 100%;
text-align: center;
display: grid;
place-content: center;
}
.empty {
opacity: 0.5;
}
.piptext {
color: rgba(255, 255, 255, 0.13);
font-weight: 600;
font-size: 4vh;
}
/* =================== TITLES ===================*/
.infoContainer > div {
overflow: hidden;
}
.infoContainer,
.clockContainer,
.countdownContainer {
background-color: rgba(255, 255, 255, 0.05);
padding: 1vh 1vw;
}
.todayContainer {
background-color: rgba(255, 255, 255, 0.07);
padding: 2.5vh 2vw;
}
.infoContainer,
.todayContainer {
border-radius: 1vw;
}
.infoContainer {
grid-area: info;
display: grid;
grid-template-rows: 3vh minmax(0, 1fr);
grid-template-columns: 3fr 1fr;
grid-template-areas:
'titl qr'
'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;
}
/* =================== SCHEDULE ===================*/
.todayContainer {
grid-area: schd;
display: grid;
grid-template-rows: 5vh 1fr 3vh;
height: 100%;
overflow: hidden;
max-width: 100%;
}
/* =================== 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,63 @@
@use '../../../styles/main' as *;
@use '../../../styles/viewers' as *;
.container__gray,
.container__grayFinished {
@include viewer-container;
display: grid;
grid-template-columns: 20vw 20vw 18vw 3vw 1fr;
grid-template-rows: 60vh 1fr 1fr 1fr;
grid-template-areas:
' pip pip pip . schd'
' titl titl titl . schd'
' info info clck . schd'
' info info time . schd';
gap: 1vw;
padding: 1vw;
}
.eventTitle {
grid-area: titl;
@include viewer-event-title;
}
.pip {
grid-area: pip;
background-color: $bg-black-100;
border: 1px solid $bg-black-200;
width: 100%;
text-align: center;
display: grid;
place-content: center;
}
.empty {
opacity: 0.5;
}
.piptext {
color: $bg-gray-900;
font-weight: 600;
font-size: 4vh;
}
.infoContainer,
.clockContainer,
.countdownContainer {
background-color: $bg-gray-1000;
padding: 1vh 1vw;
}
.todayContainer {
grid-area: schd;
}
.countdownContainer {
grid-area: clck;
border-radius: 1vw 1vw 0 0;
}
.clockContainer {
border-radius: 0 0 1vw 1vw;
}
@@ -65,51 +65,51 @@ const Lower = (props) => {
// Check for user options
useEffect(() => {
// create aux
let options = {};
const options = {};
// preset: selector
// Should be a number 1-n
let p = parseInt(searchParams.get('preset'));
const p = parseInt(searchParams.get('preset'));
if (!isNaN(p)) setPreset(p);
// size: multiplier
// Should be a number 0.0-n
let s = searchParams.get('size');
const s = searchParams.get('size');
if (s) options.size = s;
// transitionIn: seconds
// Should be a number 0-n
let t = parseInt(searchParams.get('transition'));
const t = parseInt(searchParams.get('transition'));
if (!isNaN(t)) options.transitionIn = t;
// textColour: string
// Should be a hex string '#ffffff'
let c = searchParams.get('text');
const c = searchParams.get('text');
if (c) options.textColour = `#${c}`;
// bgColour: string
// Should be a hex string '#ffffff'
let b = searchParams.get('bg');
const b = searchParams.get('bg');
if (b) options.bgColour = `#${b}`;
// key: string
// Should be a hex string '#00FF00' with key colour
let k = searchParams.get('key');
const k = searchParams.get('key');
if (k) options.keyColour = `#${k}`;
// fadeOut: seconds
// Should be a number 0-n
let f = parseInt(searchParams.get('fadeout'));
const f = parseInt(searchParams.get('fadeout'));
if (!isNaN(f)) options.fadeOut = f;
// x: pixels
// Should be a number 0-n
let x = parseInt(searchParams.get('x'));
const x = parseInt(searchParams.get('x'));
if (!isNaN(x)) options.posX = x;
// y: pixels
// Should be a number 0-n
let y = parseInt(searchParams.get('y'));
const y = parseInt(searchParams.get('y'));
if (!isNaN(y)) options.posY = y;
setLowerOptions({

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