Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 085e862be9 | |||
| 384a6298cf | |||
| 0398db9945 | |||
| 2560b0ca2a | |||
| 2e0c86c20a | |||
| 61742d1549 | |||
| 220319be8a |
@@ -40,9 +40,6 @@ dist/
|
||||
ontime-db
|
||||
ontime-external/
|
||||
|
||||
# working database
|
||||
apps/server/src/preloaded-db/db.json
|
||||
|
||||
# versioning file
|
||||
**/ONTIME_VERSION.js
|
||||
|
||||
|
||||
@@ -105,23 +105,19 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
>
|
||||
<IoLockClosedOutline />
|
||||
Editor
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</Link>
|
||||
<ClientLink to='cuesheet' current={location.pathname === '/cuesheet'}>
|
||||
<IoLockClosedOutline />
|
||||
Cuesheet
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</ClientLink>
|
||||
<ClientLink to='op' current={location.pathname === '/op'}>
|
||||
<IoLockClosedOutline />
|
||||
Operator
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</ClientLink>
|
||||
<hr className={style.separator} />
|
||||
{navigatorConstants.map((route) => (
|
||||
<ClientLink key={route.url} to={route.url} current={location.pathname === `/${route.url}`}>
|
||||
{route.label}
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</ClientLink>
|
||||
))}
|
||||
</DrawerBody>
|
||||
@@ -147,6 +143,7 @@ function ClientLink(props: PropsWithChildren<ClientLinkProps>) {
|
||||
return (
|
||||
<button className={classes} tabIndex={0} onClick={(event) => handleLinks(event, to)}>
|
||||
{children}
|
||||
<IoArrowUp className={style.linkIcon} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
export default function useElectronEvent() {
|
||||
const isElectron = window?.process?.type === 'renderer';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const sendToElectron = (channel: string, args?: string | Record<string, unknown>) => {
|
||||
if (isElectron) {
|
||||
window?.ipcRenderer.send(channel, args);
|
||||
const isElectron = window.process?.type === 'renderer';
|
||||
const ipcRenderer = isElectron ? window.require('electron').ipcRenderer : null;
|
||||
|
||||
export default function useElectronEvent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const sendToElectron = useCallback((channel: string, args?: string | Record<string, unknown>) => {
|
||||
if (isElectron && ipcRenderer) {
|
||||
ipcRenderer.send(channel, args);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// listen to requests to change the editor location
|
||||
useEffect(() => {
|
||||
if (isElectron) {
|
||||
ipcRenderer.on('request-editor-location', (_event: unknown, location: string) => {
|
||||
navigate(location, { relative: 'route' });
|
||||
});
|
||||
}
|
||||
|
||||
// Clean the listener after the component is dismounted
|
||||
return () => {
|
||||
ipcRenderer?.removeAllListeners();
|
||||
};
|
||||
}, [navigate]);
|
||||
|
||||
return { isElectron, sendToElectron };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const navigatorConstants = [
|
||||
{ url: 'timer', label: 'Timer' },
|
||||
{ url: 'clock', label: 'Clock' },
|
||||
{ url: 'minimal', label: 'Minimal Timer' },
|
||||
{ url: 'clock', label: 'Wall Clock' },
|
||||
{ url: 'backstage', label: 'Backstage' },
|
||||
{ url: 'timeline', label: 'Timeline (beta)' },
|
||||
{ url: 'public', label: 'Public' },
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"timer"
|
||||
],
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "main.js",
|
||||
"main": "src/main.js",
|
||||
"devDependencies": {
|
||||
"electron": "^31.2.0",
|
||||
"electron-builder": "^24.13.3",
|
||||
@@ -77,7 +77,7 @@
|
||||
},
|
||||
"files": [
|
||||
"**/*",
|
||||
"assets/",
|
||||
"src/assets/",
|
||||
"!**/{yarn.lock,yarn-error.log}",
|
||||
"!**/{pnpm-lock.yaml}",
|
||||
"!**/{test,tests,__test__,__tests__}",
|
||||
@@ -85,7 +85,7 @@
|
||||
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "./assets/"
|
||||
"buildResources": "./src/assets/"
|
||||
},
|
||||
"extraResources": [
|
||||
{
|
||||
@@ -112,13 +112,6 @@
|
||||
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "../server/src/preloaded-db/",
|
||||
"to": "extraResources/preloaded-db/",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "../server/src/external/",
|
||||
"to": "extraResources/external/",
|
||||
|
||||
|
Before Width: | Height: | Size: 567 B After Width: | Height: | Size: 567 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
@@ -7,7 +7,7 @@ module.exports = {
|
||||
production: (port = 4001) => `http://localhost:${port}`,
|
||||
},
|
||||
server: {
|
||||
pathToEntrypoint: '../extraResources/server/index.cjs',
|
||||
pathToEntrypoint: '../../extraResources/server/index.cjs',
|
||||
},
|
||||
assets: {
|
||||
pathToAssets: './assets/',
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* This file contains a list of constants that may need to be resolved at runtime
|
||||
*/
|
||||
const path = require('path');
|
||||
|
||||
const { version } = require('../package.json');
|
||||
|
||||
const electronConfig = require('./electron.config.js');
|
||||
|
||||
// external links
|
||||
const linkToDocs = 'https://docs.getontime.no/';
|
||||
const linkToGitHub = 'https://github.com/cpvalente/ontime';
|
||||
const linkToDiscord = 'https://discord.com/invite/eje3CSUEXm';
|
||||
|
||||
// environment and platform constants
|
||||
const env = process.env.NODE_ENV || 'production';
|
||||
const isProduction = env === 'production';
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isLinux = process.platform === 'linux';
|
||||
|
||||
const releaseTag = `v${version}`;
|
||||
|
||||
/** path to server directory */
|
||||
const nodePath = isProduction
|
||||
? path.join(__dirname, electronConfig.server.pathToEntrypoint)
|
||||
: path.join(__dirname, '../../server/dist/index.cjs');
|
||||
|
||||
/**
|
||||
* Resolves correct URL for client
|
||||
* @param {number | undefined} port - the port at which the server is running
|
||||
* @returns {string}
|
||||
*/
|
||||
const getClientUrl = (port) =>
|
||||
isProduction ? electronConfig.reactAppUrl.production(port) : electronConfig.reactAppUrl.development(port);
|
||||
|
||||
/**
|
||||
* Resolves correct URL for server
|
||||
* @param {number | undefined} port - the port at which the server is running
|
||||
* @returns {string}
|
||||
*/
|
||||
const getServerUrl = (port) => `http://localhost:${port}`;
|
||||
|
||||
/** Resolves URL path to download resources */
|
||||
const downloadPath = '/data/db/';
|
||||
|
||||
/**
|
||||
* @description Returns public path depending on OS
|
||||
* This is the correct path for the app running in production mode
|
||||
*/
|
||||
function getAppDataPath() {
|
||||
if (isMac) {
|
||||
return path.join(process.env.HOME, 'Library', 'Application Support', 'Ontime');
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
return path.join(process.env.APPDATA, 'Ontime');
|
||||
}
|
||||
|
||||
if (isLinux) {
|
||||
return path.join(process.env.HOME, '.Ontime');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
const projectsPath = path.join(getAppDataPath(), 'projects');
|
||||
const corruptProjectsPath = path.join(getAppDataPath(), 'corrupt files');
|
||||
const crashLogPath = path.join(getAppDataPath(), 'crash logs');
|
||||
const stylesPath = path.join(getAppDataPath(), 'styles');
|
||||
const externalPath = path.join(getAppDataPath(), 'external');
|
||||
|
||||
/** path to tray icon */
|
||||
const trayIcon = path.join(__dirname, electronConfig.assets.pathToAssets, 'background.png');
|
||||
/** path to app icon directory */
|
||||
const appIcon = path.join(__dirname, electronConfig.assets.pathToAssets, 'logo.png');
|
||||
|
||||
module.exports = {
|
||||
linkToDocs,
|
||||
linkToGitHub,
|
||||
linkToDiscord,
|
||||
env,
|
||||
isProduction,
|
||||
isMac,
|
||||
isWindows,
|
||||
releaseTag,
|
||||
nodePath,
|
||||
getClientUrl,
|
||||
getServerUrl,
|
||||
projectsPath,
|
||||
corruptProjectsPath,
|
||||
crashLogPath,
|
||||
stylesPath,
|
||||
externalPath,
|
||||
downloadPath,
|
||||
trayIcon,
|
||||
appIcon,
|
||||
};
|
||||
@@ -1,18 +1,20 @@
|
||||
const { app, BrowserWindow, Menu, globalShortcut, Tray, dialog, ipcMain, shell, Notification } = require('electron');
|
||||
const path = require('path');
|
||||
const electronConfig = require('./electron.config');
|
||||
const { version } = require('./package.json');
|
||||
const { getApplicationMenu } = require('./src/menu/applicationMenu.js');
|
||||
|
||||
const env = process.env.NODE_ENV || 'production';
|
||||
const isProduction = env === 'production';
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isWindows = process.platform === 'win32';
|
||||
const { getApplicationMenu } = require('./menu/applicationMenu.js');
|
||||
const { getTrayMenu } = require('./menu/trayMenu.js');
|
||||
|
||||
// path to server
|
||||
const nodePath = isProduction
|
||||
? path.join(__dirname, electronConfig.server.pathToEntrypoint)
|
||||
: path.join(__dirname, '../server/dist/index.cjs');
|
||||
const electronConfig = require('./electron.config.js');
|
||||
const {
|
||||
env,
|
||||
isProduction,
|
||||
isWindows,
|
||||
nodePath,
|
||||
getClientUrl,
|
||||
trayIcon,
|
||||
appIcon,
|
||||
getServerUrl,
|
||||
} = require('./external.js');
|
||||
|
||||
if (!isProduction) {
|
||||
console.log(`Electron running in ${env} environment`);
|
||||
@@ -20,10 +22,13 @@ if (!isProduction) {
|
||||
process.traceProcessWarnings = true;
|
||||
}
|
||||
|
||||
// path to icons
|
||||
const trayIcon = path.join(__dirname, electronConfig.assets.pathToAssets, 'background.png');
|
||||
const appIcon = path.join(__dirname, electronConfig.assets.pathToAssets, 'logo.png');
|
||||
let loaded = 'Nothing loaded';
|
||||
/** Flag holds server loading state */
|
||||
let loaded = 'Ontime running';
|
||||
|
||||
/**
|
||||
* Flag whether user has requested a quit
|
||||
* Used to coordinate window closes without exit
|
||||
*/
|
||||
let isQuitting = false;
|
||||
|
||||
// initialise
|
||||
@@ -56,13 +61,13 @@ async function startBackend() {
|
||||
|
||||
/**
|
||||
* @description utility function to create a notification
|
||||
* @param title
|
||||
* @param text
|
||||
* @param {string} title - Notification title
|
||||
* @param {string} body - Notification body
|
||||
*/
|
||||
function showNotification(title, text) {
|
||||
function showNotification(title, body) {
|
||||
new Notification({
|
||||
title,
|
||||
body: text,
|
||||
body,
|
||||
silent: true,
|
||||
}).show();
|
||||
}
|
||||
@@ -108,6 +113,14 @@ function escalateError(error) {
|
||||
dialog.showErrorBox('An unrecoverable error occurred', error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows electron to ask react app redirect
|
||||
* @param string location
|
||||
*/
|
||||
function redirectWindow(location) {
|
||||
win.webContents.send('request-editor-location', location);
|
||||
}
|
||||
|
||||
// Ensure there isn't another instance of the app running already
|
||||
const lock = app.requestSingleInstanceLock();
|
||||
if (!lock) {
|
||||
@@ -141,7 +154,7 @@ function createWindow() {
|
||||
skipTaskbar: true,
|
||||
});
|
||||
splash.setIgnoreMouseEvents(true);
|
||||
const splashPath = path.join('file://', __dirname, '/src/splash/splash.html');
|
||||
const splashPath = path.join('file://', __dirname, '/splash/splash.html');
|
||||
splash.loadURL(splashPath);
|
||||
|
||||
win = new BrowserWindow({
|
||||
@@ -156,7 +169,7 @@ function createWindow() {
|
||||
enableWebSQL: false,
|
||||
darkTheme: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, './src/preload.js'),
|
||||
preload: path.join(__dirname, './preload.js'),
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
@@ -173,18 +186,13 @@ app.whenReady().then(() => {
|
||||
}
|
||||
|
||||
createWindow();
|
||||
|
||||
startBackend()
|
||||
.then((port) => {
|
||||
// Load page served by node or use React dev run
|
||||
const clientUrl = isProduction
|
||||
? electronConfig.reactAppUrl.production(port)
|
||||
: electronConfig.reactAppUrl.development(port);
|
||||
|
||||
const template = getApplicationMenu(isMac, askToQuit, clientUrl, `v${version}`, (path) => {
|
||||
win.loadURL(`${clientUrl}/${path}`);
|
||||
});
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
const clientUrl = getClientUrl(port);
|
||||
const serverUrl = getServerUrl(port);
|
||||
const menu = getApplicationMenu(askToQuit, clientUrl, serverUrl, redirectWindow, (url) =>
|
||||
win.webContents.downloadURL(url),
|
||||
);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
win
|
||||
@@ -229,13 +237,9 @@ app.whenReady().then(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// create tray
|
||||
// create tray and set its context menu
|
||||
tray = new Tray(trayIcon);
|
||||
|
||||
// Define context menu
|
||||
const { getTrayMenu } = require('./src/menu/trayMenu.js');
|
||||
const trayMenuTemplate = getTrayMenu(bringToFront, askToQuit);
|
||||
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
|
||||
const trayContextMenu = getTrayMenu(bringToFront, askToQuit);
|
||||
tray.setContextMenu(trayContextMenu);
|
||||
});
|
||||
|
||||
@@ -261,7 +265,7 @@ ipcMain.on('shutdown', () => {
|
||||
/**
|
||||
* Handles requests to set window properties
|
||||
*/
|
||||
ipcMain.on('set-window', (event, arg) => {
|
||||
ipcMain.on('set-window', (_event, arg) => {
|
||||
switch (arg) {
|
||||
case 'show-dev':
|
||||
win.webContents.openDevTools({ mode: 'detach' });
|
||||
@@ -274,6 +278,10 @@ ipcMain.on('set-window', (event, arg) => {
|
||||
/**
|
||||
* Handles requests to open external links
|
||||
*/
|
||||
ipcMain.on('send-to-link', (event, arg) => {
|
||||
shell.openExternal(arg);
|
||||
ipcMain.on('send-to-link', (_event, arg) => {
|
||||
try {
|
||||
shell.openExternal(arg);
|
||||
} catch (_error) {
|
||||
/** unhandled error */
|
||||
}
|
||||
});
|
||||
@@ -1,185 +1,312 @@
|
||||
const { shell } = require('electron');
|
||||
const { Menu, shell } = require('electron');
|
||||
|
||||
const {
|
||||
linkToGitHub,
|
||||
linkToDocs,
|
||||
linkToDiscord,
|
||||
isProduction,
|
||||
isMac,
|
||||
releaseTag,
|
||||
projectsPath,
|
||||
corruptProjectsPath,
|
||||
crashLogPath,
|
||||
stylesPath,
|
||||
externalPath,
|
||||
downloadPath,
|
||||
} = require('../external');
|
||||
|
||||
/**
|
||||
* Build description of application menu
|
||||
* @param {boolean} isMac - Whether the target platform is mac
|
||||
* Creates the application menu
|
||||
* @param {function} askToQuit - function for quitting process
|
||||
* @param {string} clientUrl - base url for the application
|
||||
* @param {string} serverUrl - base url for the application
|
||||
* @param {function} redirectWindow - function to redirect main window content
|
||||
* @param {function} download - function to download a resource from url
|
||||
* @returns {Menu} - application menu
|
||||
*/
|
||||
function getApplicationMenu(isMac, askToQuit, urlBase, version, redirectWindow) {
|
||||
return [
|
||||
...(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: 'Public',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/public`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Lower Thirds',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/lower`);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Timer',
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/timer`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Clock',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/clock`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Minimal Timer',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/minimal`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Backstage',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/backstage`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Timeline (beta)',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/timeline`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Studio Clock',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/studio`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Countdown',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/countdown`);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Editor',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/editor`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Cuesheet',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/cuesheet`);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Operator',
|
||||
click: async () => {
|
||||
await shell.openExternal(`${urlBase}/op`);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ 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: 'About',
|
||||
click: () => redirectWindow('editor?settings=about'),
|
||||
},
|
||||
{
|
||||
label: version,
|
||||
click: () => redirectWindow('editor?settings=about'),
|
||||
},
|
||||
{
|
||||
label: 'See on github',
|
||||
click: async () => {
|
||||
await shell.openExternal('https://github.com/cpvalente/ontime');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Online documentation',
|
||||
click: async () => {
|
||||
await shell.openExternal('https://docs.getontime.no/');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
function getApplicationMenu(askToQuit, clientUrl, serverUrl, redirectWindow, download) {
|
||||
const template = [
|
||||
...(isMac ? [makeMacMenu(askToQuit)] : []),
|
||||
makeFileMenu(serverUrl, redirectWindow, download),
|
||||
makeViewMenu(clientUrl),
|
||||
makeSettingsMenu(redirectWindow),
|
||||
makeHelpMenu(redirectWindow),
|
||||
...(isProduction ? [] : [{ label: 'Dev', submenu: [{ role: 'toggleDevTools' }] }]),
|
||||
];
|
||||
return Menu.buildFromTemplate(template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function generates the app menu (macOS only)
|
||||
* @param {function} askToQuit - function for quitting process
|
||||
* @returns {Object}
|
||||
*/
|
||||
function makeMacMenu(askToQuit) {
|
||||
return {
|
||||
label: 'Ontime',
|
||||
submenu: [
|
||||
{ role: 'about', label: 'About Ontime' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide', label: 'Hide Ontime' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => askToQuit(),
|
||||
accelerator: isMac ? 'Cmd+Q' : 'Alt+F4',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function generates the file menu
|
||||
* @param {string} serverUrl - base url for the application
|
||||
* @param {function} redirectWindow - function to redirect main window content
|
||||
* @param {function} download - function to download a resource from url
|
||||
* @returns {Object}
|
||||
*/
|
||||
function makeFileMenu(serverUrl, redirectWindow, download) {
|
||||
const downloadProject = () => {
|
||||
try {
|
||||
download(serverUrl + downloadPath);
|
||||
} catch (_error) {
|
||||
/** unhandled error */
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'New project...',
|
||||
click: () => redirectWindow('/editor?settings=project__manage&create=true'),
|
||||
},
|
||||
{
|
||||
label: 'Edit project info',
|
||||
click: () => redirectWindow('/editor?settings=project__data'),
|
||||
},
|
||||
{
|
||||
label: 'Manage projects...',
|
||||
click: () => redirectWindow('/editor?settings=project__manage'),
|
||||
},
|
||||
{
|
||||
label: 'Download project',
|
||||
click: downloadProject,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Open directory',
|
||||
submenu: [
|
||||
makeItemOpenInDesktop('Projects', projectsPath),
|
||||
makeItemOpenInDesktop('Corrupted projects', corruptProjectsPath),
|
||||
makeItemOpenInDesktop('Crash logs', crashLogPath),
|
||||
makeItemOpenInDesktop('CSS override', stylesPath),
|
||||
makeItemOpenInDesktop('External', externalPath),
|
||||
],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: isMac ? 'close' : 'quit' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function generates the views menu
|
||||
* @param {string} clientUrl - base url for the application
|
||||
* @returns {Object}
|
||||
*/
|
||||
function makeViewMenu(clientUrl) {
|
||||
return {
|
||||
label: 'Views',
|
||||
submenu: [
|
||||
makeItemOpenInBrowser('Public', `${clientUrl}/public`),
|
||||
makeItemOpenInBrowser('Lower Thirds', `${clientUrl}/lower`),
|
||||
{ type: 'separator' },
|
||||
makeItemOpenInBrowser('Timer', `${clientUrl}/timer`),
|
||||
makeItemOpenInBrowser('Minimal Timer', `${clientUrl}/minimal`),
|
||||
makeItemOpenInBrowser('Clock', `${clientUrl}/clock`),
|
||||
makeItemOpenInBrowser('Backstage', `${clientUrl}/backstage`),
|
||||
makeItemOpenInBrowser('Timeline (beta)', `${clientUrl}/timeline`),
|
||||
makeItemOpenInBrowser('Studio Clock', `${clientUrl}/studio`),
|
||||
makeItemOpenInBrowser('Countdown', `${clientUrl}/countdown`),
|
||||
{ type: 'separator' },
|
||||
makeItemOpenInBrowser('Editor', `${clientUrl}/editor`),
|
||||
makeItemOpenInBrowser('Cuesheet', `${clientUrl}/cuesheet`),
|
||||
makeItemOpenInBrowser('Operator', `${clientUrl}/op`),
|
||||
|
||||
{ type: 'separator' },
|
||||
{ role: 'forceReload' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function generates the settings menu
|
||||
* @param {function} redirectWindow - function to redirect main window content
|
||||
* @returns {Object}
|
||||
*/
|
||||
function makeSettingsMenu(redirectWindow) {
|
||||
return {
|
||||
label: 'Settings',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open Settings',
|
||||
accelerator: 'CommandOrControl+,',
|
||||
click: () => redirectWindow('/editor?settings=project'),
|
||||
},
|
||||
{
|
||||
label: 'Project',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Project data',
|
||||
click: () => redirectWindow('/editor?settings=project__data'),
|
||||
},
|
||||
{
|
||||
label: 'Manage projects',
|
||||
click: () => redirectWindow('/editor?settings=project__manage'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'App Settings',
|
||||
submenu: [
|
||||
{
|
||||
label: 'General settings',
|
||||
click: () => redirectWindow('/editor?settings=general__settings'),
|
||||
},
|
||||
{
|
||||
label: 'Editor settings',
|
||||
click: () => redirectWindow('/editor?settings=general__editor'),
|
||||
},
|
||||
{
|
||||
label: 'View settings',
|
||||
click: () => redirectWindow('/editor?settings=general__view'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Feature Settings',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Custom fields',
|
||||
click: () => redirectWindow('/editor?settings=feature_settings__custom'),
|
||||
},
|
||||
{
|
||||
label: 'URL presets',
|
||||
click: () => redirectWindow('/editor?settings=feature_settings__urlpresets'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Data Sources',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Import spreadsheet',
|
||||
click: () => redirectWindow('/editor?settings=sources__xlsx'),
|
||||
},
|
||||
{
|
||||
label: 'Sync with Google Sheet',
|
||||
click: () => redirectWindow('/editor?settings=sources__gsheet'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Integrations',
|
||||
submenu: [
|
||||
{
|
||||
label: 'OSC settings',
|
||||
click: () => redirectWindow('/editor?settings=integrations__osc'),
|
||||
},
|
||||
{
|
||||
label: 'HTTP settings',
|
||||
click: () => redirectWindow('/editor?settings=integrations__http'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Network',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Event log',
|
||||
click: () => redirectWindow('/editor?settings=network__log'),
|
||||
},
|
||||
{
|
||||
label: 'Manage cleints',
|
||||
click: () => redirectWindow('/editor?settings=network__clients'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function generates the help menu
|
||||
* @param {function} redirectWindow - function to redirect main window content
|
||||
* @returns {Object}
|
||||
*/
|
||||
function makeHelpMenu(redirectWindow) {
|
||||
return {
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: `Ontime ${releaseTag}`,
|
||||
click: () => redirectWindow('/editor?settings=about'),
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
makeItemOpenInBrowser('See on github', linkToGitHub),
|
||||
makeItemOpenInBrowser('Online documentation', linkToDocs),
|
||||
makeItemOpenInBrowser('Join us on Discord', linkToDiscord),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to safely open a URL in the default browser
|
||||
* @param {string} label
|
||||
* @param {string} url
|
||||
* @returns {object} - MenuItem
|
||||
*/
|
||||
function makeItemOpenInBrowser(label, url) {
|
||||
return {
|
||||
label: `${label} ↗`,
|
||||
click: async () => {
|
||||
try {
|
||||
await shell.openExternal(url);
|
||||
} catch (_error) {
|
||||
/** unhandled error */
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to open a file in the OS explorer / finder
|
||||
* @param {string} label
|
||||
* @param {string} path
|
||||
* @returns {object} - MenuItem
|
||||
*/
|
||||
function makeItemOpenInDesktop(label, path) {
|
||||
return {
|
||||
label,
|
||||
click: () => {
|
||||
try {
|
||||
shell.openPath(path);
|
||||
} catch (_error) {
|
||||
/** unhandled error */
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { getApplicationMenu };
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
const { Menu } = require('electron');
|
||||
|
||||
/**
|
||||
* Build description of tray context menu
|
||||
* Creates the application tray menu
|
||||
* @param {function} showApp - function for making the window visible
|
||||
* @param {function} askToQuit - function for quitting process
|
||||
* @returns {Menu} - application tray menu
|
||||
*/
|
||||
function getTrayMenu(showApp, askToQuit) {
|
||||
return [
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show App',
|
||||
click: () => showApp(),
|
||||
click: showApp,
|
||||
},
|
||||
{
|
||||
label: 'Shutdown',
|
||||
click: () => askToQuit(),
|
||||
click: askToQuit,
|
||||
},
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
module.exports = { getTrayMenu };
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="../../assets/logo.png" />
|
||||
<img src="../assets/logo.png" />
|
||||
<h1>ontime · event timers</h1>
|
||||
<div class="lds-ellipsis">
|
||||
<div></div>
|
||||
|
||||
@@ -6,18 +6,18 @@ test.describe('test view navigation feature', () => {
|
||||
await page.goto('http://localhost:4001/');
|
||||
await page.locator('data-test-id=timer-view');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation__menu');
|
||||
await page.getByRole('link', { name: 'Clock', exact: true }).click();
|
||||
await page.locator('data-test-id=clock-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/clock');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation__menu');
|
||||
await page.getByRole('link', { name: 'Minimal Timer' }).click();
|
||||
await page.locator('data-test-id=minimal-timer');
|
||||
await expect(page).toHaveURL('http://localhost:4001/minimal');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation__menu');
|
||||
await page.getByRole('link', { name: 'Wall Clock', exact: true }).click();
|
||||
await page.locator('data-test-id=clock-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/clock');
|
||||
|
||||
await page.getByRole('button', { name: 'toggle menu' }).click();
|
||||
await page.locator('data-test-id=navigation__menu');
|
||||
await page.getByRole('link', { name: 'Backstage' }).click();
|
||||
@@ -55,4 +55,4 @@ test.describe('test view navigation feature', () => {
|
||||
await page.locator('data-test-id=timer-view');
|
||||
await expect(page).toHaveURL('http://localhost:4001/timer');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||