mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-04 23:18:01 +00:00
ux: macOS (#251)
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "1.9.5",
|
||||
"version": "1.9.6",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.3.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]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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.9.5",
|
||||
"version": "1.9.6",
|
||||
"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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user