Merge remote-tracking branch 'origin/master' into v1

This commit is contained in:
cv
2022-11-22 21:47:31 +01:00
15 changed files with 289 additions and 181 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime-ui", "name": "ontime-ui",
"version": "1.9.3", "version": "1.9.6",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@chakra-ui/react": "2.3.4", "@chakra-ui/react": "2.3.4",
+16 -17
View File
@@ -8,6 +8,7 @@ import { AppContextProvider } from 'common/context/AppContext';
import { LoggingProvider } from 'common/context/LoggingContext'; import { LoggingProvider } from 'common/context/LoggingContext';
import { ontimeQueryClient } from './common/queryClient'; import { ontimeQueryClient } from './common/queryClient';
import useElectronEvent from './common/hooks/useElectronEvent';
import theme from './theme/theme'; import theme from './theme/theme';
import AppRouter from './AppRouter'; import AppRouter from './AppRouter';
@@ -15,30 +16,28 @@ import AppRouter from './AppRouter';
import('typeface-open-sans'); import('typeface-open-sans');
function App() { function App() {
const { isElectron, sendToElectron } = useElectronEvent();
// Handle keyboard shortcuts const handleKeyPress = useCallback((event) => {
const handleKeyPress = useCallback((e) => { // handle held key
// handle held key if (event.repeat) return;
if (e.repeat) return; // check if the alt key is pressed
// check if the alt key is pressed if (event.altKey) {
if (e.altKey) { if (event.code === 'KeyT') {
if (e.key === 't' || e.key === 'T') {
// if we are in electron
if (window.process?.type === 'renderer') {
// ask to see debug // ask to see debug
window.ipcRenderer.send('set-window', 'show-dev'); sendToElectron('set-window', 'show-dev');
} }
} }
} },[]);
}, []);
useEffect(() => { useEffect(() => {
// attach the event listener if (isElectron) {
document.addEventListener('keydown', handleKeyPress); document.addEventListener('keydown', handleKeyPress);
}
// remove the event listener
return () => { return () => {
document.removeEventListener('keydown', handleKeyPress); if (isElectron) {
document.removeEventListener('keydown', handleKeyPress);
}
}; };
}, [handleKeyPress]); }, [handleKeyPress]);
@@ -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,
}
@@ -1,24 +1,27 @@
@use '../../../theme/main' as *; @use '../../../theme/main';
@use '../../../theme/viewerDefs' as *;
.label { .label {
@include card-label; font-size: 1.3vw;
color: var(--accent-color-override, $accent-color);
} }
.title, .title,
.subtitle, .subtitle,
.presenter { .presenter {
@include ellipsis; @include main.ellipsis;
} }
.title { .title {
@include card-title; color: $title-color;
font-weight: 600;
font-size: 2.5vw; font-size: 2.5vw;
flex: 1; flex: 1;
} }
.subtitle, .subtitle,
.presenter { .presenter {
color: $subtitle-gray; color: $subtitle-color;
} }
.subtitle { .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>
</>
);
}
+10 -10
View File
@@ -63,29 +63,29 @@ export default function MenuBar(props: MenuBarProps) {
// Handle keyboard shortcuts // Handle keyboard shortcuts
const handleKeyPress = useCallback( const handleKeyPress = useCallback(
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
// skip if not electron
if (!isElectron) return;
// handle held key // handle held key
if (event.repeat) return; if (event.repeat) return;
// check if the ctrl key is pressed // check if the ctrl key is pressed
if (event.ctrlKey) { if (event.ctrlKey || event.metaKey) {
// ctrl + , (settings) // ctrl + , (settings)
if (event.key === ',') { if (event.key === ',') {
if (isElectron) { // open if not open
// open if not open isSettingsOpen ? onSettingsClose() : onSettingsOpen();
isSettingsOpen ? onSettingsClose() : onSettingsOpen();
}
} }
} }
}, },
[isElectron, isSettingsOpen, onSettingsClose, onSettingsOpen] [isElectron, isSettingsOpen, onSettingsClose, onSettingsOpen],
); );
useEffect(() => { useEffect(() => {
document.addEventListener('keydown', handleKeyPress); if (isElectron) {
document.addEventListener('keydown', handleKeyPress);
}
return () => { return () => {
document.removeEventListener('keydown', handleKeyPress); if (isElectron) {
document.removeEventListener('keydown', handleKeyPress);
}
}; };
}, [handleKeyPress]); }, [handleKeyPress]);
+11 -12
View File
@@ -70,35 +70,34 @@ export default function Rundown(props) {
// Handle keyboard shortcuts // Handle keyboard shortcuts
const handleKeyPress = useCallback( const handleKeyPress = useCallback(
(e) => { (event) => {
// handle held key // handle held key
// handle held key if (event.repeat) return;
if (e.repeat) return;
// Check if the alt key is pressed // Check if the alt key is pressed
if (e.altKey && (!e.ctrlKey || !e.shiftKey)) { if (event.altKey && (!event.ctrlKey || !event.shiftKey)) {
// Arrow down // Arrow down
if (e.keyCode === 40) { if (event.keyCode === 40) {
if (cursor < entries.length - 1) moveCursorDown(); if (cursor < entries.length - 1) moveCursorDown();
} }
// Arrow up // Arrow up
if (e.keyCode === 38) { if (event.keyCode === 38) {
if (cursor > 0) moveCursorUp(); if (cursor > 0) moveCursorUp();
} }
// E // E
if (e.key === 'e' || e.key === 'E') { if (event.code === "KeyE") {
e.preventDefault(); event.preventDefault();
if (cursor == null) return; if (cursor == null) return;
insertAtCursor('event', cursor); insertAtCursor('event', cursor);
} }
// D // D
if (e.key === 'd' || e.key === 'D') { if (event.code === "KeyD") {
e.preventDefault(); event.preventDefault();
if (cursor == null) return; if (cursor == null) return;
insertAtCursor('delay', cursor); insertAtCursor('delay', cursor);
} }
// B // B
if (e.key === 'b' || e.key === 'B') { if (event.code === "KeyB") {
e.preventDefault(); event.preventDefault();
if (cursor == null) return; if (cursor == null) return;
insertAtCursor('block', cursor); insertAtCursor('block', cursor);
} }
-13
View File
@@ -63,8 +63,6 @@ $error-red: #e53e3e;
//////////////////////////////////// viewers //////////////////////////////////// viewers
$title-white: #fffd; $title-white: #fffd;
$title-gray: #ddd;
$subtitle-gray: #aaa;
//////////////////////////////////// block elements //////////////////////////////////// block elements
$bg-container-over: #0b1521; $bg-container-over: #0b1521;
@@ -81,17 +79,6 @@ $block-delay-border: #d69e2e55;
$block-block-color: #7347AD; $block-block-color: #7347AD;
$block-border: 1px solid $bg-gray-1100; $block-border: 1px solid $bg-gray-1100;
//////////////////////////////////// viewer cards
@mixin card-title {
color: $title-gray;
font-weight: 600;
}
@mixin card-label {
font-size: 1.3vw;
color: $ontime-pink;
}
//////////////////////////////////// utils //////////////////////////////////// utils
@mixin ellipsis { @mixin ellipsis {
+209 -58
View File
@@ -11,25 +11,30 @@ const {
} = require('electron'); } = require('electron');
const path = require('path'); const path = require('path');
const electronConfig = require('./electron.config'); const electronConfig = require('./electron.config');
if (process.env.NODE_ENV === undefined) {
process.env.NODE_ENV = 'production';
}
const isProduction = process.env.NODE_ENV === 'production';
let loaded = 'Nothing loaded'; const env = process.env.NODE_ENV || 'production';
let isQuitting = false; const isProduction = env === 'production';
const isMac = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
// path to server
const nodePath = isProduction const nodePath = isProduction
? path.join('file://', __dirname, '../', 'extraResources', 'src/app.js') ? path.join('file://', __dirname, '../', 'extraResources', 'src/app.js')
: path.join('file://', __dirname, '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;
(async () => { (async () => {
try { try {
const { startServer, startOSCServer } = await import(nodePath); const { startServer, startOSCServer } = await import(nodePath);
// Start express server // Start express server
loaded = await startServer(); loaded = await startServer();
// Start OSC Server (API) // Start OSC Server
await startOSCServer(); await startOSCServer();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -37,10 +42,6 @@ const nodePath = isProduction
} }
})(); })();
// 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 * @description utility function to create a notification
* @param title * @param title
@@ -54,6 +55,26 @@ function showNotification(title, text) {
}).show(); }).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 win;
let splash; let splash;
let tray = null; let tray = null;
@@ -76,7 +97,6 @@ if (!lock) {
} }
function createWindow() { function createWindow() {
// create a new `splash`-Window
splash = new BrowserWindow({ splash = new BrowserWindow({
width: 333, width: 333,
height: 333, height: 333,
@@ -85,7 +105,10 @@ function createWindow() {
resizable: false, resizable: false,
frame: false, frame: false,
alwaysOnTop: true, alwaysOnTop: true,
focusable: false,
skipTaskbar: true,
}); });
splash.setIgnoreMouseEvents(true);
splash.loadURL(`file://${__dirname}/electron/splash/splash.html`); splash.loadURL(`file://${__dirname}/electron/splash/splash.html`);
win = new BrowserWindow({ win = new BrowserWindow({
@@ -115,16 +138,10 @@ function createWindow() {
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
app.whenReady().then(() => { app.whenReady().then(() => {
// Set app title in windows // Set app title in windows
if (process.platform === 'win32') { if (isWindows) {
app.setAppUserModelId(app.name); app.setAppUserModelId(app.name);
} }
// allow usual quit in mac
if (process.platform === 'darwin') {
globalShortcut.register('Command+Q', () => {
win.send('user-request-shutdown');
});
}
createWindow(); createWindow();
// register global shortcuts // register global shortcuts
@@ -135,13 +152,6 @@ app.whenReady().then(() => {
win.focus(); win.focus();
}); });
// recreate window if no others open
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// give the nodejs server some time // give the nodejs server some time
setTimeout(() => { setTimeout(() => {
// Load page served by node // Load page served by node
@@ -165,15 +175,18 @@ app.whenReady().then(() => {
}); });
}, electronConfig.appIni.mainWindowWait); }, electronConfig.appIni.mainWindowWait);
// recreate window if no others open
app.on('activate', () => {
win.show();
});
// Hide on close // Hide on close
win.on('close', function (event) { win.on('close', function (event) {
event.preventDefault(); event.preventDefault();
if (!isQuitting) { if (!isQuitting) {
showNotification('Window Closed', 'App running in background'); showNotification('Window Closed', 'App running in background');
win.hide(); win.hide();
return false;
} }
return true;
}); });
// create tray // create tray
@@ -190,35 +203,184 @@ app.whenReady().then(() => {
}, },
{ {
label: 'Shutdown', label: 'Shutdown',
click: () => { click: () => askToQuit(),
win.destroy();
app.quit();
},
}, },
]; ];
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate); const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
tray.setContextMenu(trayContextMenu); 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);
// unregister shortcuts before quitting // unregister shortcuts before quitting
app.once('will-quit', () => { app.once('will-quit', () => {
globalShortcut.unregisterAll(); globalShortcut.unregisterAll();
}); });
// destroy tray icon before quit
app.once('before-quit', () => {
tray.destroy();
});
// Get messages from react // Get messages from react
// Test message // Test message
ipcMain.on('test-message', (event, arg) => { ipcMain.on('test-message', (event, arg) => {
@@ -227,7 +389,7 @@ ipcMain.on('test-message', (event, arg) => {
// Ask for main window reload // Ask for main window reload
// Test message // Test message
ipcMain.on('reload', (event, arg) => { ipcMain.on('reload', () => {
if (win) { if (win) {
win.reload(); win.reload();
} }
@@ -236,18 +398,7 @@ ipcMain.on('reload', (event, arg) => {
// Terminate // Terminate
ipcMain.on('shutdown', () => { ipcMain.on('shutdown', () => {
console.log('Got IPC shutdown'); console.log('Got IPC shutdown');
appShutdown();
// terminate node service
(async () => {
const { shutdown } = await import(nodePath);
// Shutdown service
await shutdown();
})();
isQuitting = true;
tray.destroy();
win.destroy();
app.quit();
}); });
// Window manipulation // Window manipulation
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ontime", "name": "ontime",
"version": "1.9.3", "version": "1.9.6",
"author": "Carlos Valente", "author": "Carlos Valente",
"description": "Time keeping for live events", "description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime", "repository": "https://github.com/cpvalente/ontime",
-1
View File
@@ -163,7 +163,6 @@ export const startServer = async (overrideConfig = null) => {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
export const shutdown = async () => { export const shutdown = async () => {
console.log('Node service shutdown');
// shutdown express server // shutdown express server
server.close(); server.close();
+8 -2
View File
@@ -3,7 +3,7 @@ import { Server } from 'node-osc';
let oscServer = null; let oscServer = null;
/** /**
* @description utilty function to shutdown osc server * @description utility function to shut down osc server
*/ */
export const shutdownOSCServer = () => { export const shutdownOSCServer = () => {
if (oscServer != null) oscServer.close(); if (oscServer != null) oscServer.close();
@@ -23,7 +23,7 @@ export const initiateOSC = (config) => {
// message should look like /ontime/{path}/{args} where // message should look like /ontime/{path}/{args} where
// ontime: fixed message for app // ontime: fixed message for app
// path: command to be called // 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 // split message
const [, address, path] = msg[0].split('/'); const [, address, path] = msg[0].split('/');
@@ -145,6 +145,12 @@ export const initiateOSC = (config) => {
break; break;
} }
case 'get-playback': {
const playback = global.timer.state;
global.timer.sendOsc('playback', playback);
break;
}
default: { default: {
global.timer.warning('RX', `OSC IN: unhandled message ${path}`); global.timer.warning('RX', `OSC IN: unhandled message ${path}`);
break; break;
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ontime-server", "name": "ontime-server",
"type": "module", "type": "module",
"version": "1.4.0", "version": "1.4.2",
"dependencies": { "dependencies": {
"body-parser": "^1.20.0", "body-parser": "^1.20.0",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
+1 -22
View File
@@ -1,6 +1,6 @@
import jest from 'jest-mock'; import jest from 'jest-mock';
import { dbModel } from '../../models/dataModel.js'; import { dbModel } from '../../models/dataModel.js';
import { isStringEmpty, parseExcel, parseJson, validateEvent } from '../parser.js'; import { parseExcel, parseJson, validateEvent } from '../parser.js';
import { makeString, validateDuration } from '../parserUtils.js'; import { makeString, validateDuration } from '../parserUtils.js';
import { parseAliases, parseUserFields, parseViews } from '../parserFunctions.js'; import { parseAliases, parseUserFields, parseViews } from '../parserFunctions.js';
@@ -886,24 +886,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);
});
}
});
});
+2 -15
View File
@@ -19,19 +19,6 @@ import { generateId } from './generate_id.js';
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
export const JSON_MIME = 'application/json'; 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 * @description Excel array parser
* @param {array} excelData - array with excel sheet * @param {array} excelData - array with excel sheet
@@ -102,9 +89,9 @@ export const parseExcel = async (excelData) => {
} else if (j === subtitleIndex) { } else if (j === subtitleIndex) {
event.subtitle = column; event.subtitle = column;
} else if (j === isPublicIndex) { } else if (j === isPublicIndex) {
event.isPublic = isStringEmpty(column); event.isPublic = Boolean(column);
} else if (j === skipIndex) { } else if (j === skipIndex) {
event.skip = isStringEmpty(column); event.skip = Boolean(column);
} else if (j === notesIndex) { } else if (j === notesIndex) {
event.note = column; event.note = column;
} else if (j === colourIndex) { } else if (j === colourIndex) {