* 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,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
@@ -1,172 +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 .... . 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,
.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: 1vw;
}
.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,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({
@@ -18,7 +18,7 @@ export default function StudioClock(props) {
const activeIndicators = [...Array(12).keys()];
const secondsIndicators = [...Array(60).keys()];
const MAX_TITLES = 8;
const MAX_TITLES = 10;
// Set window title
useEffect(() => {
@@ -45,7 +45,7 @@ export default function StudioClock(props) {
<div
ref={ref}
className={style.nextTitle}
style={{ fontSize, height: '100px', width: '100%', maxWidth: '680px' }}
style={{ fontSize, height: '10vh', width: '100%', maxWidth: '82%' }}
>
{title.titleNext}
</div>
@@ -58,7 +58,7 @@ export default function StudioClock(props) {
key={i}
className={style.hours__active}
style={{
transform: `rotate(${(360 / 12) * i - 90}deg) translateX(380px)`,
transform: `rotate(${(360 / 12) * i - 90}deg) translateX(40vh)`,
}}
/>
))}
@@ -67,7 +67,7 @@ export default function StudioClock(props) {
key={i}
className={i <= secondsNow ? style.min__active : style.min}
style={{
transform: `rotate(${(360 / 60) * i - 90}deg) translateX(415px)`,
transform: `rotate(${(360 / 60) * i - 90}deg) translateX(43vh)`,
}}
/>
))}
@@ -94,8 +94,8 @@ export default function StudioClock(props) {
}
StudioClock.propTypes = {
title: PropTypes.string,
time: PropTypes.number,
title: PropTypes.object,
time: PropTypes.object,
backstageEvents: PropTypes.array,
selectedId: PropTypes.string,
nextId: PropTypes.string,
@@ -3,6 +3,18 @@
src: local('digital-7'), url('./../../../assets/fonts/digital-7.monoitalic.ttf') format('truetype') ;
}
/* =============== CLOCK STUFF ==================*/
$clock-size: 90vh;
$size-hours: min(3vh, 20px);
$half-hours: min(1.5vh, 10px);
$size-min: min(2.5vh, 18px);
$half-min: min(1.25vh, 9px);
$red-active: #c53030;
$red-idle: #300000;
$cyan-active: #0ff;
$cyan-idle: #0aa;
.container {
margin: 0;
box-sizing: border-box; /* reset */
@@ -14,66 +26,27 @@
background: #000;
display: grid;
grid-template-columns: 1000px 1fr;
gap: 50px;
grid-template-columns: 95vh 1fr;
gap: 2vw;
grid-template-areas: "clck schd";
/* =============== CLOCK STUFF ==================*/
$clock-size: 900px;
$size-hours: 20px;
$half-hours: 10px;
$size-min: 18px;
$half-min: 9px;
$red-active: #c53030;
$red-idle: #300000;
$cyan-active: #0ff;
$cyan-idle: #0aa;
.clockContainer {
display: grid;
place-content: center;
display: flex;
flex-direction: column;
align-items: center;
grid-area: clck;
width: $clock-size;
height: $clock-size;
aspect-ratio: 1;
text-align: center;
position: relative;
margin: auto;
margin: 4vh auto;
font-family: digital-clock, monospace;
text-transform: uppercase;
.time {
margin-top: 175px;
color: $red-active;
font-size: 300px;
line-height: 0.8em;
}
.nextTitle:after,
.nextCountdown:after,
.nextCountdown__overtime:after {
content: '\200b';
}
.nextTitle {
color:$cyan-idle;
line-height: 100px;
}
.nextCountdown,
.nextCountdown__overtime {
font-size: 100px;
line-height: 1em;
}
.nextCountdown {
color: $cyan-active;
text-shadow: rgb(0,100,100) 0 0 20px;
}
.nextCountdown::before {
content: '-';
}
.nextCountdown__overtime {
color: darken($red-active, 10%);
}
.indicators {
.indicators {
position: absolute;
top: 0;
width: 100%;
height: 100%;
@@ -85,6 +58,7 @@
position: absolute;
background: $red-idle;
}
.min,
.min__active {
min-height: $size-min;
@@ -92,60 +66,100 @@
top: calc(50% - #{$half_min});
left: calc(50% - #{$half_min});
}
.hours,
.hours__active{
min-height:$size-hours;
.hours__active {
min-height: $size-hours;
width: $size-hours;
top: calc(50% - #{$half_hours});
left: calc(50% - #{$half_hours});
}
.min__active,
.hours__active {
background: $red-active;
box-shadow: 0 0 10px 2px rgba(255,0,0,0.25);
box-shadow: 0 0 10px 2px rgba(255, 0, 0, 0.25);
}
}
.time {
color: $red-active;
font-size: calc(#{$clock-size} / 3);
margin-top: calc(50% - calc(#{$clock-size} / 7));
line-height: 0.8em;
}
.nextTitle:after,
.nextCountdown:after,
.nextCountdown__overtime:after {
content: '\200b';
}
.nextTitle {
color: $cyan-idle;
text-align: center;
}
.nextCountdown,
.nextCountdown__overtime {
font-size: 10vh;
line-height: 1em;
}
.nextCountdown {
color: $cyan-active;
text-shadow: rgb(0, 100, 100) 0 0 20px;
}
.nextCountdown::before {
content: '-';
}
.nextCountdown__overtime {
color: darken($red-active, 10%);
}
}
/* ============= SCHEDULE STUFF =================*/
.scheduleContainer {
grid-area: schd;
margin: 50px 0;
padding-right: 50px;
margin: 4vh 0;
font-family: digital-clock, monospace;
text-transform: uppercase;
.onAir,
.onAir__idle{
padding-bottom: 50px;
font-size: 170px;
.onAir__idle {
padding-bottom: 2vh;
font-size: 15vh;
line-height: 0.9em;
}
.onAir {
color: $red-active;
}
.onAir__idle {
color: $red-idle;
}
.schedule {
ul {
color: $cyan-idle;
font-size: 3.75vh;
line-height: 1em;
list-style: none;
font-size: 40px;
}
li {
margin-bottom: 0.5em;
padding-left: 0.5em;
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 1.5vh;
}
.now {
color: $cyan-active;
}
.next {
color: $red-active;
}
@@ -153,14 +167,14 @@
}
}
@media only screen and (max-width: 1600px) {
@media only screen and (max-width: 1200px) {
.container {
display: grid;
grid-template-areas: "clck";
grid-template-columns: 100%;
place-content: center;
}
.scheduleContainer{
.scheduleContainer {
display: none;
}
}
@@ -14,7 +14,7 @@ export default function MinimalTimer(props) {
const showOverlay = pres.text !== '' && pres.visible;
const isPlaying = time.playstate !== 'pause';
const timer = formatDisplay(time.running, true);
const clean = timer.replaceAll(':', '');
const clean = timer.replace('/:/g', '');
return (
<div className={time.finished ? style.containerFinished : style.container}>
@@ -43,7 +43,7 @@
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.85);
background-color: $bg-overlay;
z-index: -1;
opacity: 0;
transition: 0.5s;
@@ -7,7 +7,7 @@
overflow: hidden;
width: 100%; /* restrict the page width to viewport */
background: radial-gradient(circle, $bg-black-gradient 0%, $bg-black 80%);
background: $bg-black;
height: 100vh;
color: $title-white;
display: grid;
@@ -31,7 +31,7 @@
.nowContainer,
.nextContainer {
background-color: rgba(255, 255, 255, 0.05);
background-color: $bg-gray-1000;
padding: 1vh 2vw;
border-radius: 1vw;
max-width: 100%;
@@ -98,7 +98,7 @@
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.85);
background-color: $bg-overlay;
z-index: -1;
opacity: 0;
transition: 0.5s;