Compare commits

...

9 Commits

Author SHA1 Message Date
Carlos Valente 085e862be9 refactor: show electron dev tools 2024-10-06 21:32:29 +02:00
Carlos Valente 384a6298cf refactor: arrow used in new window navigation 2024-10-06 21:32:08 +02:00
Carlos Valente 0398db9945 refactor: navigate in electron 2024-10-06 21:21:16 +02:00
Carlos Valente 2560b0ca2a refactor: remove unused db 2024-10-06 20:34:58 +02:00
Carlos Valente 2e0c86c20a feat: open asset directories 2024-10-06 20:34:58 +02:00
Carlos Valente 61742d1549 refactor: collocate view navigation 2024-10-06 20:34:58 +02:00
Carlos Valente 220319be8a refactor: improve native menu links 2024-10-06 20:34:54 +02:00
Carlos Valente ea5de337cf feat: allow download of current project 2024-10-03 22:28:47 +02:00
Carlos Valente a08ea3fee9 refactor: create project form state from params 2024-10-03 22:28:47 +02:00
24 changed files with 529 additions and 259 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
@@ -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,4 +1,5 @@
import { ChangeEvent, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Button, Input } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
@@ -13,14 +14,17 @@ import ProjectList from './ProjectList';
import style from './ProjectPanel.module.scss';
export default function ManageProjects() {
const [isCreatingProject, setIsCreatingProject] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const [error, setError] = useState('');
const [loading, setLoading] = useState<'import' | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const isCreatingProject = searchParams.get('create') === 'true';
const handleToggleCreate = () => {
setIsCreatingProject((prev) => !prev);
searchParams.set('create', isCreatingProject ? 'false' : 'true');
setSearchParams(searchParams);
};
const handleSelectFile = () => {
@@ -49,7 +53,8 @@ export default function ManageProjects() {
};
const handleCloseForm = () => {
setIsCreatingProject(false);
searchParams.delete('create');
setSearchParams(searchParams);
};
return (
+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();
}
@@ -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 */
}
});
+305 -178
View File
@@ -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 };
+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>
@@ -58,6 +58,19 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
}
}
/**
* Allows downloading of current project file
*/
export async function currentProjectDownload(_req: Request, res: Response) {
const { filename, pathToFile } = await projectService.getCurrentProject();
res.download(pathToFile, filename, (error) => {
if (error) {
const message = getErrorMessage(error);
res.status(500).send({ message });
}
});
}
/**
* Allows downloading of project files
*/
+2
View File
@@ -2,6 +2,7 @@ import express from 'express';
import {
createProjectFile,
currentProjectDownload,
deleteProjectFile,
duplicateProjectFile,
getInfo,
@@ -23,6 +24,7 @@ import {
export const router = express.Router();
router.get('/', currentProjectDownload);
router.post('/download', validateFilenameBody, projectDownload);
router.post('/upload', uploadProjectFile, postProjectFile);
@@ -51,6 +51,13 @@ function init() {
ensureDirectory(resolveCorruptDirectory);
}
export async function getCurrentProject() {
const filename = await getLastLoadedProject();
const pathToFile = getPathToProject(filename);
return { filename, pathToFile };
}
/**
* Private function loads a demo project
* to be composed in the loading functions
+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');
});
});
});