diff --git a/client/package.json b/client/package.json
index 39073f3ab..115566c0b 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
- "version": "1.9.3",
+ "version": "1.9.6",
"private": true,
"dependencies": {
"@chakra-ui/react": "2.3.4",
diff --git a/client/src/App.jsx b/client/src/App.jsx
index 0fe24c0c5..12bdabd3a 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -8,6 +8,7 @@ import { AppContextProvider } from 'common/context/AppContext';
import { LoggingProvider } from 'common/context/LoggingContext';
import { ontimeQueryClient } from './common/queryClient';
+import useElectronEvent from './common/hooks/useElectronEvent';
import theme from './theme/theme';
import AppRouter from './AppRouter';
@@ -15,30 +16,28 @@ import AppRouter from './AppRouter';
import('typeface-open-sans');
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]);
diff --git a/client/src/common/components/title-card/TitleCard.jsx b/client/src/common/components/title-card/TitleCard.jsx
deleted file mode 100644
index 3f6f9a598..000000000
--- a/client/src/common/components/title-card/TitleCard.jsx
+++ /dev/null
@@ -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 (
- <>
-
{label}
- {title}
- {presenter}
- {subtitle}
- >
- );
-}
-
-TitleCard.propTypes = {
- label: PropTypes.string,
- title: PropTypes.string,
- subtitle: PropTypes.string,
- presenter: PropTypes.string,
-}
diff --git a/client/src/common/components/title-card/TitleCard.module.scss b/client/src/common/components/title-card/TitleCard.scss
similarity index 56%
rename from client/src/common/components/title-card/TitleCard.module.scss
rename to client/src/common/components/title-card/TitleCard.scss
index 3dbc6e430..ab95f01f7 100644
--- a/client/src/common/components/title-card/TitleCard.module.scss
+++ b/client/src/common/components/title-card/TitleCard.scss
@@ -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 {
diff --git a/client/src/common/components/title-card/TitleCard.tsx b/client/src/common/components/title-card/TitleCard.tsx
new file mode 100644
index 000000000..cc93529e1
--- /dev/null
+++ b/client/src/common/components/title-card/TitleCard.tsx
@@ -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 (
+ <>
+ {label}
+ {title}
+ {presenter}
+ {subtitle}
+ >
+ );
+}
diff --git a/client/src/features/menu/MenuBar.tsx b/client/src/features/menu/MenuBar.tsx
index 3485a96bf..678f88ca1 100644
--- a/client/src/features/menu/MenuBar.tsx
+++ b/client/src/features/menu/MenuBar.tsx
@@ -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]);
diff --git a/client/src/features/rundown/Rundown.jsx b/client/src/features/rundown/Rundown.jsx
index 59dbb10ce..d2c38d16f 100644
--- a/client/src/features/rundown/Rundown.jsx
+++ b/client/src/features/rundown/Rundown.jsx
@@ -70,35 +70,34 @@ export default function Rundown(props) {
// Handle keyboard shortcuts
const handleKeyPress = useCallback(
- (e) => {
+ (event) => {
// handle held key
- // 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 < entries.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);
}
diff --git a/client/src/theme/_main.scss b/client/src/theme/_main.scss
index 0986e39b4..5b4ed0ded 100644
--- a/client/src/theme/_main.scss
+++ b/client/src/theme/_main.scss
@@ -63,8 +63,6 @@ $error-red: #e53e3e;
//////////////////////////////////// viewers
$title-white: #fffd;
-$title-gray: #ddd;
-$subtitle-gray: #aaa;
//////////////////////////////////// block elements
$bg-container-over: #0b1521;
@@ -81,17 +79,6 @@ $block-delay-border: #d69e2e55;
$block-block-color: #7347AD;
$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
@mixin ellipsis {
diff --git a/server/main.js b/server/main.js
index e9d0ea52b..23d3a764a 100644
--- a/server/main.js
+++ b/server/main.js
@@ -11,25 +11,30 @@ const {
} = require('electron');
const path = require('path');
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';
-let isQuitting = false;
+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;
+
(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);
@@ -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
* @param title
@@ -54,6 +55,26 @@ 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;
@@ -76,7 +97,6 @@ if (!lock) {
}
function createWindow() {
- // create a new `splash`-Window
splash = new BrowserWindow({
width: 333,
height: 333,
@@ -85,7 +105,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({
@@ -115,16 +138,10 @@ function createWindow() {
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
@@ -135,13 +152,6 @@ 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
@@ -165,15 +175,18 @@ app.whenReady().then(() => {
});
}, electronConfig.appIni.mainWindowWait);
+ // recreate window if no others open
+ app.on('activate', () => {
+ win.show();
+ });
+
// Hide on close
win.on('close', function (event) {
event.preventDefault();
if (!isQuitting) {
showNotification('Window Closed', 'App running in background');
win.hide();
- return false;
}
- return true;
});
// create tray
@@ -190,35 +203,184 @@ 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);
+
// unregister shortcuts before quitting
app.once('will-quit', () => {
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) => {
@@ -227,7 +389,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();
}
@@ -236,18 +398,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
diff --git a/server/package.json b/server/package.json
index 3791a19f9..0c9b7ffcf 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "ontime",
- "version": "1.9.3",
+ "version": "1.9.6",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
diff --git a/server/src/app.js b/server/src/app.js
index 4b2c8bbbe..3615f7633 100644
--- a/server/src/app.js
+++ b/server/src/app.js
@@ -163,7 +163,6 @@ export const startServer = async (overrideConfig = null) => {
* @return {Promise}
*/
export const shutdown = async () => {
- console.log('Node service shutdown');
// shutdown express server
server.close();
diff --git a/server/src/controllers/OscController.js b/server/src/controllers/OscController.js
index 0233a547e..cd6d15794 100644
--- a/server/src/controllers/OscController.js
+++ b/server/src/controllers/OscController.js
@@ -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;
diff --git a/server/src/package.json b/server/src/package.json
index db96160c3..9211c46df 100644
--- a/server/src/package.json
+++ b/server/src/package.json
@@ -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",
diff --git a/server/src/utils/__tests__/parser.tests.js b/server/src/utils/__tests__/parser.tests.js
index 8a9e4e25f..12e90feb7 100644
--- a/server/src/utils/__tests__/parser.tests.js
+++ b/server/src/utils/__tests__/parser.tests.js
@@ -1,6 +1,6 @@
import jest from 'jest-mock';
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 { 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);
- });
- }
- });
-});
diff --git a/server/src/utils/parser.js b/server/src/utils/parser.js
index 1cb594999..8ca95a372 100644
--- a/server/src/utils/parser.js
+++ b/server/src/utils/parser.js
@@ -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 = 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) {