Compare commits

...

4 Commits

Author SHA1 Message Date
Carlos Valente 7c1975b415 refactor: remove unused db 2024-10-04 13:47:09 +02:00
Carlos Valente 16013e60ac feat: open asset directories 2024-10-04 13:47:09 +02:00
Carlos Valente 992de6ddc1 refactor: collocate view navigation 2024-10-04 13:47:09 +02:00
Carlos Valente d001fa6eed refactor: improve native menu links 2024-10-04 13:47:02 +02:00
18 changed files with 406 additions and 246 deletions
-3
View File
@@ -40,9 +40,6 @@ dist/
ontime-db
ontime-external/
# working database
apps/server/src/preloaded-db/db.json
# versioning file
**/ONTIME_VERSION.js
+1 -1
View File
@@ -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' },
+3 -10
View File
@@ -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/',
+98
View File
@@ -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();
}
@@ -141,7 +146,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 +161,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 +178,19 @@ 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,
(path) => {
win.loadURL(`${clientUrl}/${path}`);
},
(url) => win.webContents.downloadURL(url),
);
Menu.setApplicationMenu(menu);
win
@@ -229,13 +235,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 +263,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 +276,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 */
}
});
+241 -178
View File
@@ -1,185 +1,248 @@
const { shell } = require('electron');
const { Menu, shell } = require('electron');
const {
linkToGitHub,
linkToDocs,
linkToDiscord,
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),
];
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('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: 'App settings',
click: () => redirectWindow('editor?settings=general'),
},
{
label: 'Editor settings',
click: () => redirectWindow('editor?settings=general__editor'),
},
{
label: 'View settings',
click: () => redirectWindow('editor?settings=general__view'),
},
{
label: 'Integrations',
click: () => redirectWindow('editor?settings=integrations'),
},
{
label: 'Network',
click: () => redirectWindow('editor?settings=network'),
},
],
};
}
/**
* 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 };
+8 -5
View File
@@ -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 };
+1 -1
View File
@@ -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>
+7 -7
View File
@@ -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');
});
});
});