mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-03 06:28:01 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d2b88b626 | |||
| 409bc65427 | |||
| 0ff6556e4d | |||
| 2021d9394d | |||
| dfef6d4e79 | |||
| b5a521f625 | |||
| b9bc4627df | |||
| 159ba82e00 | |||
| 45cfb633de | |||
| 4a83f2554d | |||
| 2215319cd1 | |||
| 766ff40baa | |||
| 61cbce780d | |||
| cf963f6e05 | |||
| 320834beab |
@@ -188,3 +188,4 @@ jobs:
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "1.8.2",
|
||||
"version": "1.9.8",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.3.2",
|
||||
@@ -15,6 +15,7 @@
|
||||
"autosize": "^5.0.1",
|
||||
"axios": "^0.27.2",
|
||||
"color": "^4.2.3",
|
||||
"csv-stringify": "^6.2.3",
|
||||
"framer-motion": "^7.3.2",
|
||||
"jotai": "^1.7.8",
|
||||
"luxon": "^3.0.1",
|
||||
@@ -34,7 +35,7 @@
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"stylelint": "npx stylelint \"**/*.scss\"\n",
|
||||
"test": "vitest ",
|
||||
"test": "vitest",
|
||||
"test:pipeline": "vitest run"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -80,4 +81,4 @@
|
||||
"vite-tsconfig-paths": "^3.5.0",
|
||||
"vitest": "^0.23.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-17
@@ -7,6 +7,7 @@ import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
|
||||
import { AppContextProvider } from './common/context/AppContext';
|
||||
import SocketProvider from './common/context/socketContext';
|
||||
import useElectronEvent from './common/hooks/useElectronEvent';
|
||||
import theme from './theme/theme';
|
||||
import AppRouter from './AppRouter';
|
||||
|
||||
@@ -15,30 +16,28 @@ import('typeface-open-sans');
|
||||
export const ontimeQueryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
const { isElectron, sendToElectron } = useElectronEvent();
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback((e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// check if the alt key is pressed
|
||||
if (e.altKey) {
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
// if we are in electron
|
||||
if (window.process?.type === 'renderer') {
|
||||
const handleKeyPress = useCallback((event) => {
|
||||
// handle held key
|
||||
if (event.repeat) return;
|
||||
// check if the alt key is pressed
|
||||
if (event.altKey) {
|
||||
if (event.code === 'KeyT') {
|
||||
// ask to see debug
|
||||
window.ipcRenderer.send('set-window', 'show-dev');
|
||||
sendToElectron('set-window', 'show-dev');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
},[]);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
if (isElectron) {
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
if (isElectron) {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ export const STATIC_PORT = 4001;
|
||||
export const EVENT_TABLE = ['event'];
|
||||
export const ALIASES = ['aliases'];
|
||||
export const USERFIELDS = ['userFields'];
|
||||
export const EVENTS_TABLE = ['events'];
|
||||
export const EVENTS_TABLE_KEY = 'events';
|
||||
export const EVENTS_TABLE = [EVENTS_TABLE_KEY];
|
||||
export const APP_TABLE = ['appinfo'];
|
||||
export const OSC_SETTINGS = ['oscSettings'];
|
||||
export const APP_SETTINGS = ['appSettings'];
|
||||
@@ -29,4 +30,4 @@ export const playbackURL = `${serverURL}/playback`;
|
||||
export const ontimeURL = `${serverURL}/ontime`;
|
||||
|
||||
export const stylesPath = 'external/styles/override.css';
|
||||
export const overrideStylesURL = `serverURL/${stylesPath}`;
|
||||
export const overrideStylesURL = `${serverURL}/${stylesPath}`;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
export const mirrorViewersAtom = atomWithStorage('ontime-viewers-mirrorViewers', false);
|
||||
@@ -36,7 +36,7 @@ class ErrorBoundary extends React.Component {
|
||||
render() {
|
||||
if (this.state.errorMessage) {
|
||||
return (
|
||||
<div className={style.errorContainer}>
|
||||
<div className={style.errorContainer} data-testid="error-container">
|
||||
<div>
|
||||
<p className={style.error}>:/</p>
|
||||
<p>Something went wrong</p>
|
||||
|
||||
@@ -4,10 +4,13 @@ import { IconButton } from '@chakra-ui/button';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import { IoSync } from '@react-icons/all-files/io5/IoSync';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { mirrorViewersAtom } from '../../atoms/ViewerSettings';
|
||||
import useFullscreen from '../../hooks/useFullscreen';
|
||||
|
||||
import navigatorConstants from './navigatorConstants';
|
||||
@@ -26,6 +29,7 @@ export default function NavLogo(props) {
|
||||
const { isHidden } = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
const [isMirrored, setMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setShowNav((prev) => !prev);
|
||||
@@ -57,6 +61,8 @@ export default function NavLogo(props) {
|
||||
tabIndex: 0,
|
||||
};
|
||||
|
||||
console.log('debug', isMirrored)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
|
||||
@@ -75,6 +81,12 @@ export default function NavLogo(props) {
|
||||
exit={{ opacity: 0, scaleX: 0 }}
|
||||
className={style.actions}
|
||||
>
|
||||
<IconButton
|
||||
aria-label='Mirror screen'
|
||||
icon={<IoSync />}
|
||||
onClick={() => setMirrored((prev) => !prev)}
|
||||
{...navButtonStyle}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label='Toggle Fullscreen'
|
||||
icon={isFullScreen ? <IoContract /> : <IoExpand />}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from './TitleCard.module.scss';
|
||||
|
||||
export default function TitleCard(props) {
|
||||
const { label, title, subtitle, presenter } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.label}>{label}</div>
|
||||
<div className={style.title}>{title}</div>
|
||||
<div className={style.presenter}>{presenter}</div>
|
||||
<div className={style.subtitle}>{subtitle}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
TitleCard.propTypes = {
|
||||
label: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
presenter: PropTypes.string,
|
||||
}
|
||||
+8
-5
@@ -1,24 +1,27 @@
|
||||
@use '../../../theme/main' as *;
|
||||
@use '../../../theme/main';
|
||||
@use '../../../theme/viewerDefs' as *;
|
||||
|
||||
.label {
|
||||
@include card-label;
|
||||
font-size: 1.3vw;
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle,
|
||||
.presenter {
|
||||
@include ellipsis;
|
||||
@include main.ellipsis;
|
||||
}
|
||||
|
||||
.title {
|
||||
@include card-title;
|
||||
color: $title-color;
|
||||
font-weight: 600;
|
||||
font-size: 2.5vw;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.subtitle,
|
||||
.presenter {
|
||||
color: $subtitle-gray;
|
||||
color: $subtitle-color;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -0,0 +1,21 @@
|
||||
import './TitleCard.scss';
|
||||
|
||||
interface TitleCardProps {
|
||||
label: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
presenter: string;
|
||||
}
|
||||
|
||||
export default function TitleCard(props: TitleCardProps) {
|
||||
const { label, title, subtitle, presenter } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='label'>{label}</div>
|
||||
<div className='title'>{title}</div>
|
||||
<div className='presenter'>{presenter}</div>
|
||||
<div className='subtitle'>{subtitle}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export const useFetch = (namespace, fn) => {
|
||||
const { data, status, isError, refetch } = useQuery(namespace, fn, {
|
||||
refetchInterval: refetchIntervalMs,
|
||||
cacheTime: Infinity,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
|
||||
@@ -9,6 +9,7 @@ import { EVENTS_TABLE } from '../api/apiConstants';
|
||||
export default function useMutateEvents(mutation){
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(mutation, {
|
||||
networkMode: 'always',
|
||||
onMutate: async (newEvent) => {
|
||||
// cancel ongoing queries
|
||||
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
|
||||
|
||||
@@ -39,34 +39,34 @@ export default function EventList(props) {
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(e) => {
|
||||
(event) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
if (event.repeat) return;
|
||||
// Check if the alt key is pressed
|
||||
if (e.altKey && (!e.ctrlKey || !e.shiftKey)) {
|
||||
if (event.altKey && (!event.ctrlKey || !event.shiftKey)) {
|
||||
// Arrow down
|
||||
if (e.keyCode === 40) {
|
||||
if (event.keyCode === 40) {
|
||||
if (cursor < events.length - 1) moveCursorDown();
|
||||
}
|
||||
// Arrow up
|
||||
if (e.keyCode === 38) {
|
||||
if (event.keyCode === 38) {
|
||||
if (cursor > 0) moveCursorUp();
|
||||
}
|
||||
// E
|
||||
if (e.key === 'e' || e.key === 'E') {
|
||||
e.preventDefault();
|
||||
if (event.code === "KeyE") {
|
||||
event.preventDefault();
|
||||
if (cursor == null) return;
|
||||
insertAtCursor('event', cursor);
|
||||
}
|
||||
// D
|
||||
if (e.key === 'd' || e.key === 'D') {
|
||||
e.preventDefault();
|
||||
if (event.code === "KeyD") {
|
||||
event.preventDefault();
|
||||
if (cursor == null) return;
|
||||
insertAtCursor('delay', cursor);
|
||||
}
|
||||
// B
|
||||
if (e.key === 'b' || e.key === 'B') {
|
||||
e.preventDefault();
|
||||
if (event.code === "KeyB") {
|
||||
event.preventDefault();
|
||||
if (cursor == null) return;
|
||||
insertAtCursor('block', cursor);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { EVENTS_TABLE } from 'common/api/apiConstants';
|
||||
import { EVENTS_TABLE, EVENTS_TABLE_KEY } from 'common/api/apiConstants';
|
||||
import {
|
||||
fetchAllEvents,
|
||||
requestApplyDelay,
|
||||
@@ -30,6 +30,7 @@ export default function EventListWrapper() {
|
||||
const [events, setEvents] = useState(null);
|
||||
|
||||
const addEvent = useMutation(requestPost, {
|
||||
networkMode: 'always',
|
||||
// we optimistically update here
|
||||
onMutate: async (newEvent) => {
|
||||
// cancel ongoing queries
|
||||
@@ -76,16 +77,17 @@ export default function EventListWrapper() {
|
||||
});
|
||||
|
||||
const updateEvent = useMutation(requestPut, {
|
||||
networkMode: 'always',
|
||||
// we optimistically update here
|
||||
onMutate: async (newEvent) => {
|
||||
// cancel ongoing queries
|
||||
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
|
||||
queryClient.cancelQueries([EVENTS_TABLE_KEY, newEvent.id]);
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE_KEY, newEvent.id]);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
||||
queryClient.setQueryData([EVENTS_TABLE_KEY, newEvent.id], newEvent);
|
||||
|
||||
// Return a context with the previous and new todo
|
||||
return { previousEvent, newEvent };
|
||||
@@ -93,26 +95,27 @@ export default function EventListWrapper() {
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
|
||||
queryClient.setQueryData([EVENTS_TABLE_KEY, context.newEvent.id], context.previousEvent);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: (newEvent) => {
|
||||
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
|
||||
queryClient.invalidateQueries([EVENTS_TABLE_KEY, newEvent.id]);
|
||||
},
|
||||
});
|
||||
|
||||
const patchEvent = useMutation(requestPatch, {
|
||||
networkMode: 'always',
|
||||
// we optimistically update here
|
||||
onMutate: async (newEvent) => {
|
||||
// cancel ongoing queries
|
||||
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
|
||||
queryClient.cancelQueries([EVENTS_TABLE_KEY, newEvent.id]);
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE_KEY, newEvent.id]);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
||||
queryClient.setQueryData([EVENTS_TABLE_KEY, newEvent.id], newEvent);
|
||||
|
||||
// Return a context with the previous and new todo
|
||||
return { previousEvent, newEvent };
|
||||
@@ -120,13 +123,13 @@ export default function EventListWrapper() {
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
|
||||
queryClient.setQueryData([EVENTS_TABLE_KEY, context.newEvent.id], context.previousEvent);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: (newEvent) => {
|
||||
if (newEvent) {
|
||||
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
|
||||
queryClient.invalidateQueries([EVENTS_TABLE_KEY, newEvent.id]);
|
||||
} else {
|
||||
queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
}
|
||||
@@ -134,10 +137,11 @@ export default function EventListWrapper() {
|
||||
});
|
||||
|
||||
const deleteEvent = useMutation(requestDelete, {
|
||||
networkMode: 'always',
|
||||
// we optimistically update here
|
||||
onMutate: async (eventId) => {
|
||||
// cancel ongoing queries
|
||||
queryClient.cancelQueries([EVENTS_TABLE, eventId]);
|
||||
queryClient.cancelQueries([EVENTS_TABLE_KEY, eventId]);
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
||||
@@ -163,6 +167,7 @@ export default function EventListWrapper() {
|
||||
});
|
||||
|
||||
const deleteAllEvents = useMutation(requestDeleteAll, {
|
||||
networkMode: 'always',
|
||||
// we optimistically update here
|
||||
onMutate: async () => {
|
||||
// cancel ongoing queries
|
||||
@@ -192,6 +197,7 @@ export default function EventListWrapper() {
|
||||
});
|
||||
|
||||
const applyDelay = useMutation(requestApplyDelay, {
|
||||
networkMode: 'always',
|
||||
// Mutation finished, failed or successful
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
@@ -199,6 +205,7 @@ export default function EventListWrapper() {
|
||||
});
|
||||
|
||||
const reorderEvent = useMutation(requestReorder, {
|
||||
networkMode: 'always',
|
||||
// we optimistically update here
|
||||
onMutate: async (data) => {
|
||||
// cancel ongoing queries
|
||||
|
||||
@@ -63,29 +63,29 @@ export default function MenuBar(props: MenuBarProps) {
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
// skip if not electron
|
||||
if (!isElectron) return;
|
||||
// handle held key
|
||||
if (event.repeat) return;
|
||||
|
||||
// check if the ctrl key is pressed
|
||||
if (event.ctrlKey) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
// ctrl + , (settings)
|
||||
if (event.key === ',') {
|
||||
if (isElectron) {
|
||||
// open if not open
|
||||
isSettingsOpen ? onSettingsClose() : onSettingsOpen();
|
||||
}
|
||||
// open if not open
|
||||
isSettingsOpen ? onSettingsClose() : onSettingsOpen();
|
||||
}
|
||||
}
|
||||
},
|
||||
[isElectron, isSettingsOpen, onSettingsClose, onSettingsOpen]
|
||||
[isElectron, isSettingsOpen, onSettingsClose, onSettingsOpen],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
if (isElectron) {
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
if (isElectron) {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import style from './Modals.module.scss';
|
||||
|
||||
export default function AppSettingsModal() {
|
||||
const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings);
|
||||
const { emitError, emitWarning } = useContext(LoggingContext);
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
const [formData, setFormData] = useState(ontimePlaceholderSettings);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -79,10 +79,7 @@ export default function AppSettingsModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// set fields with error
|
||||
if (!validation.isValid) {
|
||||
emitError(`Invalid Input: ${validation.message}`);
|
||||
} else {
|
||||
if (validation.isValid) {
|
||||
await postSettings(formData);
|
||||
await refetch();
|
||||
validation?.message && emitWarning(validation.message);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { TableSettingsContext } from '../../common/context/TableSettingsContext'
|
||||
import { useFetch } from '../../common/hooks/useFetch';
|
||||
import useFullscreen from '../../common/hooks/useFullscreen';
|
||||
import { useTimerProvider } from '../../common/hooks/useSocketProvider';
|
||||
import { formatDisplay } from '../../common/utils/dateConfig';
|
||||
import { formatDisplay, millisToSeconds } from '../../common/utils/dateConfig';
|
||||
import { formatTime } from '../../common/utils/time';
|
||||
import { tooltipDelayFast } from '../../ontimeConfig';
|
||||
|
||||
@@ -22,7 +22,7 @@ import PlaybackIcon from './tableElements/PlaybackIcon';
|
||||
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function TableHeader({handleCSVExport, featureData}) {
|
||||
export default function TableHeader({ handleCSVExport, featureData }) {
|
||||
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } =
|
||||
useContext(TableSettingsContext);
|
||||
const timer = useTimerProvider();
|
||||
@@ -32,11 +32,12 @@ export default function TableHeader({handleCSVExport, featureData}) {
|
||||
const selected = !featureData.numEvents
|
||||
? 'No events'
|
||||
: `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : '-'}/${
|
||||
featureData.numEvents ? featureData.numEvents : '-'
|
||||
}`;
|
||||
featureData.numEvents ? featureData.numEvents : '-'
|
||||
}`;
|
||||
|
||||
// prepare presentation variables
|
||||
const timerNow = `${timer.running < 0 ? '-' : ''}${formatDisplay(timer.running)}`;
|
||||
const isOvertime = timer.current < 0;
|
||||
const timerNow = `${isOvertime ? '-' : ''}${formatDisplay(millisToSeconds(timer.current))}`;
|
||||
const timeNow = formatTime(timer.clock, {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { stringify } from 'csv-stringify/browser/esm/sync';
|
||||
|
||||
/**
|
||||
* @description parses a field for export
|
||||
* @param {string} field
|
||||
@@ -99,9 +101,6 @@ export const makeTable = (headerData, tableData, userFields) => {
|
||||
*/
|
||||
export const makeCSV = (arrayOfArrays) => {
|
||||
let csvData = 'data:text/csv;charset=utf-8,';
|
||||
arrayOfArrays.forEach((rowArray) => {
|
||||
const row = rowArray.join(',');
|
||||
csvData += `${row}\n`;
|
||||
});
|
||||
return csvData;
|
||||
const stringifiedData = stringify(arrayOfArrays);
|
||||
return csvData + stringifiedData;
|
||||
};
|
||||
|
||||
@@ -5,9 +5,11 @@ import Paginator from 'common/components/paginator/Paginator';
|
||||
import TitleSide from 'common/components/title-side/TitleSide';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
@@ -26,6 +28,7 @@ export default function Backstage(props) {
|
||||
const [filteredEvents, setFilteredEvents] = useState(null);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -58,7 +61,7 @@ export default function Backstage(props) {
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
return (
|
||||
<div className='backstage'>
|
||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`}>
|
||||
<NavLogo />
|
||||
|
||||
<div className='event-title'>{general.title}</div>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import {
|
||||
TimeManager,
|
||||
ViewSettings,
|
||||
} from '../../../common/models/OntimeTypes';
|
||||
import { TimeManager, ViewSettings } from '../../../common/models/OntimeTypes';
|
||||
import { OverridableOptions } from '../../../common/models/ViewTypes';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
@@ -27,6 +26,7 @@ export default function Clock(props: ClockProps) {
|
||||
const { time, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Clock';
|
||||
@@ -127,7 +127,7 @@ export default function Clock(props: ClockProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className='clock-view'
|
||||
className={`clock-view ${isMirrored ? 'mirror' : ''}`}
|
||||
style={{
|
||||
backgroundColor: userOptions.keyColour,
|
||||
color: userOptions.textColour,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import Empty from '../../../common/components/state/Empty';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
@@ -23,6 +25,7 @@ export default function Countdown(props) {
|
||||
const { backstageEvents, time, selectedId, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
const [follow, setFollow] = useState(null);
|
||||
const [runningTimer, setRunningTimer] = useState(0);
|
||||
@@ -90,7 +93,7 @@ export default function Countdown(props) {
|
||||
: formatTime(follow.timeEnd + delay, formatOptions);
|
||||
|
||||
return (
|
||||
<div className='countdown'>
|
||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`}>
|
||||
<NavLogo />
|
||||
{follow === null ? (
|
||||
<div className='event-select'>
|
||||
@@ -139,7 +142,7 @@ export default function Countdown(props) {
|
||||
>
|
||||
{formatDisplay(
|
||||
isSelected ? runningTimer : runningTimer + millisToSeconds(delay),
|
||||
isSelected || time.waiting
|
||||
isSelected || time.waiting,
|
||||
)}
|
||||
</span>
|
||||
<div className='title'>{follow?.title || 'Untitled Event'}</div>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { overrideStylesURL } from 'common/api/apiConstants';
|
||||
import { useRuntimeStylesheet } from 'common/hooks/useRuntimeStylesheet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
|
||||
import LowerClean from './LowerClean';
|
||||
import LowerLines from './LowerLines';
|
||||
|
||||
const isEqual = require('react-fast-compare');
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return isEqual(prevProps.title, nextProps.title) && isEqual(prevProps.lower, nextProps.lower);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import {
|
||||
@@ -24,6 +26,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
const { pres, time, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Minimal Timer';
|
||||
@@ -131,12 +134,13 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
const clean = timer.replace('/:/g', '');
|
||||
const showFinished = time.isNegative && !userOptions?.hideOvertime;
|
||||
|
||||
const baseClasses = `minimal-timer ${isMirrored ? 'mirror' : ''}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={showFinished ? 'minimal-timer minimal-timer--finished' : 'minimal-timer'}
|
||||
className={showFinished ? `${baseClasses} minimal-timer--finished` : baseClasses}
|
||||
style={{
|
||||
backgroundColor: userOptions.keyColour,
|
||||
color: userOptions.textColour,
|
||||
justifyContent: userOptions.justifyContent,
|
||||
alignItems: userOptions.alignItems,
|
||||
}}
|
||||
@@ -155,6 +159,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
|
||||
showFinished ? 'timer--finished' : ''
|
||||
}`}
|
||||
style={{
|
||||
color: userOptions.textColour,
|
||||
fontSize: `${(89 / (clean.length - 1)) * (userOptions.size || 1)}vw`,
|
||||
fontFamily: userOptions.font,
|
||||
top: userOptions.top,
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
import './Pip.scss';
|
||||
import { useAtom } from 'jotai';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
|
||||
const formatOptions = {
|
||||
showSeconds: true,
|
||||
@@ -25,6 +27,7 @@ export default function Pip(props) {
|
||||
const [filteredEvents, setFilteredEvents] = useState(null);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -65,7 +68,7 @@ export default function Pip(props) {
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
return (
|
||||
<div className='pip'>
|
||||
<div className={`pip ${isMirrored ? 'mirror' : ''}`}>
|
||||
<NavLogo />
|
||||
|
||||
<div className='event-title'>{general.title}</div>
|
||||
|
||||
@@ -4,9 +4,11 @@ import NavLogo from 'common/components/nav/NavLogo';
|
||||
import Paginator from 'common/components/paginator/Paginator';
|
||||
import TitleSide from 'common/components/title-side/TitleSide';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { titleVariants } from '../common/animation';
|
||||
@@ -23,6 +25,7 @@ export default function Public(props) {
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -40,7 +43,7 @@ export default function Public(props) {
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
|
||||
return (
|
||||
<div className='public-screen'>
|
||||
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`}>
|
||||
<NavLogo />
|
||||
|
||||
<div className='event-title'>{general.title}</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import NavLogo from '../../../common/components/nav/NavLogo';
|
||||
import useFitText from '../../../common/hooks/useFitText';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
@@ -28,6 +30,7 @@ export default function StudioClock(props) {
|
||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
||||
|
||||
const [schedule, setSchedule] = useState([]);
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
const activeIndicators = [...Array(12).keys()];
|
||||
const secondsIndicators = [...Array(60).keys()];
|
||||
@@ -48,13 +51,13 @@ export default function StudioClock(props) {
|
||||
showEnd: false,
|
||||
});
|
||||
setSchedule(formatted);
|
||||
}, [backstageEvents, nextId, selectedId] );
|
||||
}, [backstageEvents, nextId, selectedId]);
|
||||
|
||||
const clock = formatTime(time.clock, formatOptions);
|
||||
const [, , secondsNow] = stringFromMillis(time.clock).split(':');
|
||||
|
||||
return (
|
||||
<div className='studio-clock'>
|
||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`}>
|
||||
<NavLogo />
|
||||
<div className='clock-container'>
|
||||
<div className='studio-timer'>{clock}</div>
|
||||
@@ -65,7 +68,8 @@ export default function StudioClock(props) {
|
||||
>
|
||||
{title.titleNext}
|
||||
</div>
|
||||
<div className={time.isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}>
|
||||
<div
|
||||
className={time.isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}>
|
||||
{selectedId != null && formatDisplay(time.running)}
|
||||
</div>
|
||||
<div className='clock-indicators'>
|
||||
|
||||
@@ -5,9 +5,11 @@ import MyProgressBar from 'common/components/myProgressBar/MyProgressBar';
|
||||
import NavLogo from 'common/components/nav/NavLogo';
|
||||
import TitleCard from 'common/components/title-card/TitleCard';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
|
||||
@@ -23,6 +25,7 @@ export default function Timer(props) {
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [elapsed, setElapsed] = useState(true);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Timer';
|
||||
@@ -64,9 +67,10 @@ export default function Timer(props) {
|
||||
y: 500,
|
||||
},
|
||||
};
|
||||
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''}`;
|
||||
|
||||
return (
|
||||
<div className={time.finished ? 'stage-timer stage-timer--finished' : 'stage-timer'}>
|
||||
<div className={time.finished ? `${baseClasses} stage-timer--finished` : baseClasses}>
|
||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||
<div className='message'>{pres.text}</div>
|
||||
</div>
|
||||
|
||||
@@ -59,8 +59,6 @@ $error-red: #e53e3e;
|
||||
|
||||
//////////////////////////////////// viewers
|
||||
$title-white: #fffd;
|
||||
$title-gray: #ddd;
|
||||
$subtitle-gray: #aaa;
|
||||
|
||||
//////////////////////////////////// block elements
|
||||
$block-delay-color: #ecc94b;
|
||||
@@ -70,17 +68,6 @@ $block-block-color: #805ad5;
|
||||
$block-icon-drag: $bg-gray-100;
|
||||
$block-border: 1px solid $bg-gray-800;
|
||||
|
||||
//////////////////////////////////// viewer cards
|
||||
@mixin card-title {
|
||||
color: $title-gray;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@mixin card-label {
|
||||
font-size: 1.3vw;
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
//////////////////////////////////// utils
|
||||
|
||||
@mixin ellipsis {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.mirror {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@use 'main' as *;
|
||||
@use 'viewerCommon' as *;
|
||||
|
||||
// General styling
|
||||
$accent-color: $ontime-pink; // --accent-color-override
|
||||
|
||||
@@ -2403,6 +2403,11 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
|
||||
integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
|
||||
|
||||
csv-stringify@^6.2.3:
|
||||
version "6.2.3"
|
||||
resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-6.2.3.tgz#fefd25e66fd48f8f42f43b85a66a4663a2c3e796"
|
||||
integrity sha512-4qGjUMwnlaRc00gc2jrIYh2w/h1fo25B0mTuY9K8fBiIgtmCX3LcgUbrEGViL98Ci4Se/F5LFEtu8k+dItJVZQ==
|
||||
|
||||
data-urls@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
|
||||
|
||||
@@ -53,6 +53,11 @@ describe('validate routes', () => {
|
||||
cy.contains('Info');
|
||||
});
|
||||
|
||||
it('renders lower third with no errors', () => {
|
||||
cy.visit('http://localhost:4001/lower');
|
||||
cy.get('[data-testid="error-container"]').should('not.exist');
|
||||
});
|
||||
|
||||
it('renders studio clock', () => {
|
||||
cy.visit('http://localhost:4001/studio');
|
||||
cy.contains('ON AIR');
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
appIni: {
|
||||
mainWindowWait: 2000,
|
||||
},
|
||||
reactAppUrl: {
|
||||
development: 'http://localhost:3000/editor',
|
||||
production: 'http://localhost:4001/editor',
|
||||
},
|
||||
externalUrls: {
|
||||
help: 'https://cpvalente.gitbook.io/ontime/',
|
||||
},
|
||||
};
|
||||
+237
-68
@@ -10,27 +10,33 @@ const {
|
||||
Notification,
|
||||
} = require('electron');
|
||||
const path = require('path');
|
||||
const electronConfig = require('./electron.config');
|
||||
|
||||
if (process.env.NODE_ENV === undefined) {
|
||||
process.env.NODE_ENV = 'production';
|
||||
}
|
||||
const env = process.env.NODE_ENV;
|
||||
// environment vars
|
||||
const env = process.env.NODE_ENV || 'production';
|
||||
const isProduction = env === 'production';
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// path to server
|
||||
const nodePath = isProduction
|
||||
? path.join('file://', __dirname, '../', 'extraResources', 'src/app.js')
|
||||
: path.join('file://', __dirname, 'src/app.js');
|
||||
|
||||
// path to icons
|
||||
const trayIcon = path.join(__dirname, './assets/background.png');
|
||||
const appIcon = path.join(__dirname, './assets/logo.png');
|
||||
|
||||
let loaded = 'Nothing loaded';
|
||||
let isQuitting = false;
|
||||
|
||||
const nodePath =
|
||||
env !== 'production'
|
||||
? path.join('file://', __dirname, 'src/app.js')
|
||||
: path.join('file://', __dirname, '../', 'extraResources', 'src/app.js');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const { startServer, startOSCServer } = await import(nodePath);
|
||||
// Start express server
|
||||
loaded = await startServer();
|
||||
|
||||
// Start OSC Server (API)
|
||||
// Start OSC Server
|
||||
await startOSCServer();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -38,10 +44,6 @@ const nodePath =
|
||||
}
|
||||
})();
|
||||
|
||||
// Load Icons
|
||||
const trayIcon = path.join(__dirname, './assets/background.png');
|
||||
const appIcon = path.join(__dirname, './assets/logo.png');
|
||||
|
||||
/**
|
||||
* @description utility function to create a notification
|
||||
* @param title
|
||||
@@ -55,12 +57,33 @@ function showNotification(title, text) {
|
||||
}).show();
|
||||
}
|
||||
|
||||
function appShutdown() {
|
||||
// terminate node service
|
||||
(async () => {
|
||||
const { shutdown } = await import(nodePath);
|
||||
// Shutdown service
|
||||
await shutdown();
|
||||
})();
|
||||
|
||||
isQuitting = true;
|
||||
tray.destroy();
|
||||
win.destroy();
|
||||
app.quit();
|
||||
}
|
||||
|
||||
function askToQuit() {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.send('user-request-shutdown');
|
||||
}
|
||||
|
||||
let win;
|
||||
let splash;
|
||||
let tray = null;
|
||||
|
||||
// Ensure there isn't another instance of the app running already
|
||||
const lock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!lock) {
|
||||
dialog.showErrorBox('Multiple instances', 'An instance if the App is already running.');
|
||||
app.quit();
|
||||
@@ -76,7 +99,6 @@ if (!lock) {
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// create a new `splash`-Window
|
||||
splash = new BrowserWindow({
|
||||
width: 333,
|
||||
height: 333,
|
||||
@@ -85,7 +107,10 @@ function createWindow() {
|
||||
resizable: false,
|
||||
frame: false,
|
||||
alwaysOnTop: true,
|
||||
focusable: false,
|
||||
skipTaskbar: true,
|
||||
});
|
||||
splash.setIgnoreMouseEvents(true);
|
||||
splash.loadURL(`file://${__dirname}/electron/splash/splash.html`);
|
||||
|
||||
win = new BrowserWindow({
|
||||
@@ -112,18 +137,13 @@ function createWindow() {
|
||||
win.setMenu(null);
|
||||
}
|
||||
|
||||
app.disableHardwareAcceleration();
|
||||
app.whenReady().then(() => {
|
||||
// Set app title in windows
|
||||
if (process.platform === 'win32') {
|
||||
if (isWindows) {
|
||||
app.setAppUserModelId(app.name);
|
||||
}
|
||||
|
||||
// allow usual quit in mac
|
||||
if (process.platform === 'darwin') {
|
||||
globalShortcut.register('Command+Q', () => {
|
||||
win.send('user-request-shutdown');
|
||||
});
|
||||
}
|
||||
createWindow();
|
||||
|
||||
// register global shortcuts
|
||||
@@ -134,32 +154,33 @@ app.whenReady().then(() => {
|
||||
win.focus();
|
||||
});
|
||||
|
||||
// recreate window if no others open
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// give the nodejs server some time
|
||||
setTimeout(() => {
|
||||
// Load page served by node
|
||||
const reactApp =
|
||||
env === 'development' ? 'http://localhost:3000/editor' : 'http://localhost:4001/editor';
|
||||
const reactApp = isProduction
|
||||
? electronConfig.reactAppUrl.production
|
||||
: electronConfig.reactAppUrl.development;
|
||||
|
||||
win.loadURL(reactApp).then(() => {
|
||||
win.webContents.setBackgroundThrottling(false);
|
||||
|
||||
// window stuff
|
||||
win.show();
|
||||
win.focus();
|
||||
|
||||
splash.destroy();
|
||||
|
||||
// tray stuff
|
||||
tray.setToolTip(loaded);
|
||||
if (typeof loaded === 'string') {
|
||||
tray.setToolTip(loaded);
|
||||
} else {
|
||||
tray.setToolTip('Initialising error: please restart ontime');
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}, electronConfig.appIni.mainWindowWait);
|
||||
|
||||
// recreate window if no others open
|
||||
app.on('activate', () => {
|
||||
win.show();
|
||||
});
|
||||
|
||||
// Hide on close
|
||||
win.on('close', function (event) {
|
||||
@@ -167,13 +188,10 @@ app.whenReady().then(() => {
|
||||
if (!isQuitting) {
|
||||
showNotification('Window Closed', 'App running in background');
|
||||
win.hide();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// create tray
|
||||
// TODO: Design better icon
|
||||
tray = new Tray(trayIcon);
|
||||
|
||||
// Define context menu
|
||||
@@ -187,35 +205,197 @@ app.whenReady().then(() => {
|
||||
},
|
||||
{
|
||||
label: 'Shutdown',
|
||||
click: () => {
|
||||
win.destroy();
|
||||
app.quit();
|
||||
},
|
||||
click: () => askToQuit(),
|
||||
},
|
||||
];
|
||||
|
||||
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
|
||||
tray.setContextMenu(trayContextMenu);
|
||||
});
|
||||
|
||||
// on tray click event, show main window
|
||||
tray.on('click', function () {
|
||||
if (!win.isVisible()) {
|
||||
win.show();
|
||||
}
|
||||
win.focus();
|
||||
});
|
||||
const template = [
|
||||
...(isMac
|
||||
? [
|
||||
{
|
||||
label: 'Ontime',
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'quit',
|
||||
click: () => askToQuit(),
|
||||
accelerator: 'Cmd+Q',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [isMac ? { role: 'close' } : { role: 'quit' }],
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
...(isMac
|
||||
? [
|
||||
{ role: 'pasteAndMatchStyle' },
|
||||
{ role: 'delete' },
|
||||
{ role: 'selectAll' },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Speech',
|
||||
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
|
||||
},
|
||||
]
|
||||
: [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Views',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Ontime Views (opens in browser)',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Timer',
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/timer');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Clock',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/clock');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Minimal Timer',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/minimal');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Backstage',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/backstage');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Public',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/public');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Lower Thirds',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/lower');
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
label: 'PiP',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/pip');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Studio Clock',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/studio');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Countdown',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/countdown');
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Editor',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/editor');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Cuesheet',
|
||||
click: async () => {
|
||||
await shell.openExternal('http://localhost:4001/cuesheet');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'forceReload' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
{ role: 'minimize' },
|
||||
{ role: 'zoom' },
|
||||
...(isMac
|
||||
? [{ type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' }]
|
||||
: [{ role: 'close' }]),
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'See on github',
|
||||
click: async () => {
|
||||
await shell.openExternal('https://github.com/cpvalente/ontime');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Online documentation',
|
||||
click: async () => {
|
||||
await shell.openExternal('https://cpvalente.gitbook.io/ontime/');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
app.on('closed', (event) => {
|
||||
console.log(3, event);
|
||||
});
|
||||
|
||||
app.on('window-all-closed', (event) => {
|
||||
console.log(1, event);
|
||||
});
|
||||
|
||||
app.on('window-all-closed', (event) => {
|
||||
console.log(2, event);
|
||||
});
|
||||
|
||||
// unregister shortcuts before quitting
|
||||
app.once('will-quit', () => {
|
||||
console.log(4);
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// destroy tray icon before quit
|
||||
app.once('before-quit', () => {
|
||||
tray.destroy();
|
||||
});
|
||||
|
||||
// Get messages from react
|
||||
// Test message
|
||||
ipcMain.on('test-message', (event, arg) => {
|
||||
@@ -224,7 +404,7 @@ ipcMain.on('test-message', (event, arg) => {
|
||||
|
||||
// Ask for main window reload
|
||||
// Test message
|
||||
ipcMain.on('reload', (event, arg) => {
|
||||
ipcMain.on('reload', () => {
|
||||
if (win) {
|
||||
win.reload();
|
||||
}
|
||||
@@ -233,18 +413,7 @@ ipcMain.on('reload', (event, arg) => {
|
||||
// Terminate
|
||||
ipcMain.on('shutdown', () => {
|
||||
console.log('Got IPC shutdown');
|
||||
|
||||
// terminate node service
|
||||
(async () => {
|
||||
const { shutdown } = await import(nodePath);
|
||||
// Shutdown service
|
||||
await shutdown();
|
||||
})();
|
||||
|
||||
isQuitting = true;
|
||||
tray.destroy();
|
||||
win.destroy();
|
||||
app.quit();
|
||||
appShutdown();
|
||||
});
|
||||
|
||||
// Window manipulation
|
||||
@@ -269,7 +438,7 @@ ipcMain.on('send-to-link', (event, arg) => {
|
||||
|
||||
// send to help URL
|
||||
if (arg === 'help') {
|
||||
shell.openExternal('https://cpvalente.gitbook.io/ontime/');
|
||||
shell.openExternal(electronConfig.externalUrls.help);
|
||||
} else {
|
||||
shell.openExternal(arg);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "1.8.2",
|
||||
"version": "1.10.1",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -142,7 +142,6 @@ export const startServer = async (overrideConfig = null) => {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export const shutdown = async () => {
|
||||
console.log('Node service shutdown');
|
||||
// shutdown express server
|
||||
server.close();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Server } from 'node-osc';
|
||||
let oscServer = null;
|
||||
|
||||
/**
|
||||
* @description utilty function to shutdown osc server
|
||||
* @description utility function to shut down osc server
|
||||
*/
|
||||
export const shutdownOSCServer = () => {
|
||||
if (oscServer != null) oscServer.close();
|
||||
@@ -23,7 +23,7 @@ export const initiateOSC = (config) => {
|
||||
// message should look like /ontime/{path}/{args} where
|
||||
// ontime: fixed message for app
|
||||
// path: command to be called
|
||||
// args: extra data, only used on some of the API entries (delay, goto)
|
||||
// args: extra data, only used on some API entries (delay, goto)
|
||||
|
||||
// split message
|
||||
const [, address, path] = msg[0].split('/');
|
||||
@@ -145,6 +145,12 @@ export const initiateOSC = (config) => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'get-playback': {
|
||||
const playback = global.timer.state;
|
||||
global.timer.sendOsc('playback', playback);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
global.timer.warning('RX', `OSC IN: unhandled message ${path}`);
|
||||
break;
|
||||
|
||||
@@ -48,7 +48,8 @@ const uploadAndParse = async (file, req, res, options) => {
|
||||
} else if (result.message === 'success') {
|
||||
// explicitly write objects
|
||||
if (typeof result !== 'undefined') {
|
||||
if (!options.onlyEvents) {
|
||||
const uploadAll = options?.onlyEvents === 'false';
|
||||
if (uploadAll) {
|
||||
const mergedData = DataProvider.safeMerge(data, result.data);
|
||||
data.event = mergedData.event;
|
||||
data.settings = mergedData.settings;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.2",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.0",
|
||||
"dotenv": "^16.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import jest from 'jest-mock';
|
||||
import { dbModelv1, dbModelv1 as dbModel } from '../../models/dataModel.js';
|
||||
import { isStringEmpty, parseExcel_v1, parseJson_v1, validateEvent_v1 } from '../parser.js';
|
||||
import { parseExcel_v1, parseJson_v1, validateEvent_v1 } from '../parser.js';
|
||||
import { makeString, validateDuration } from '../parserUtils.js';
|
||||
import { parseAliases_v1, parseUserFields_v1, parseViews_v1 } from '../parserUtils_v1.js';
|
||||
|
||||
@@ -876,24 +876,3 @@ describe('test validateDuration()', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStringEmpty() function', () => {
|
||||
describe('returns true with any non empty', () => {
|
||||
const notEmpty = ['test', 'thisalso', '123', '#'];
|
||||
for (const testValue of notEmpty) {
|
||||
it(testValue, () => {
|
||||
const isEmpty = isStringEmpty(testValue);
|
||||
expect(isEmpty).toBe(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('returns true empty string or undefined', () => {
|
||||
const empty = ['', ' ', undefined, null];
|
||||
for (const testValue of empty) {
|
||||
it(`handles ${testValue}`, () => {
|
||||
const isEmpty = isStringEmpty(testValue);
|
||||
expect(isEmpty).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,19 +19,6 @@ import { generateId } from './generate_id.js';
|
||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
export const JSON_MIME = 'application/json';
|
||||
|
||||
/**
|
||||
* @description Whether a string is considered empty
|
||||
* @param value
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const isStringEmpty = (value) => {
|
||||
let v = value;
|
||||
if (typeof value === 'string') {
|
||||
v = value.replace(/\s+/g, '');
|
||||
}
|
||||
return v === '' || !v;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Excel array parser
|
||||
* @param {array} excelData - array with excel sheet
|
||||
@@ -102,9 +89,9 @@ export const parseExcel_v1 = async (excelData) => {
|
||||
} else if (j === subtitleIndex) {
|
||||
event.subtitle = column;
|
||||
} else if (j === isPublicIndex) {
|
||||
event.isPublic = isStringEmpty(column);
|
||||
event.isPublic = Boolean(column);
|
||||
} else if (j === skipIndex) {
|
||||
event.skip = isStringEmpty(column);
|
||||
event.skip = Boolean(column);
|
||||
} else if (j === notesIndex) {
|
||||
event.note = column;
|
||||
} else if (j === colourIndex) {
|
||||
|
||||
+1282
-1564
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user