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-db
ontime-external/ ontime-external/
# working database
apps/server/src/preloaded-db/db.json
# versioning file # versioning file
**/ONTIME_VERSION.js **/ONTIME_VERSION.js
@@ -105,23 +105,19 @@ function NavigationMenu(props: NavigationMenuProps) {
> >
<IoLockClosedOutline /> <IoLockClosedOutline />
Editor Editor
<IoArrowUp className={style.linkIcon} />
</Link> </Link>
<ClientLink to='cuesheet' current={location.pathname === '/cuesheet'}> <ClientLink to='cuesheet' current={location.pathname === '/cuesheet'}>
<IoLockClosedOutline /> <IoLockClosedOutline />
Cuesheet Cuesheet
<IoArrowUp className={style.linkIcon} />
</ClientLink> </ClientLink>
<ClientLink to='op' current={location.pathname === '/op'}> <ClientLink to='op' current={location.pathname === '/op'}>
<IoLockClosedOutline /> <IoLockClosedOutline />
Operator Operator
<IoArrowUp className={style.linkIcon} />
</ClientLink> </ClientLink>
<hr className={style.separator} /> <hr className={style.separator} />
{navigatorConstants.map((route) => ( {navigatorConstants.map((route) => (
<ClientLink key={route.url} to={route.url} current={location.pathname === `/${route.url}`}> <ClientLink key={route.url} to={route.url} current={location.pathname === `/${route.url}`}>
{route.label} {route.label}
<IoArrowUp className={style.linkIcon} />
</ClientLink> </ClientLink>
))} ))}
</DrawerBody> </DrawerBody>
@@ -147,6 +143,7 @@ function ClientLink(props: PropsWithChildren<ClientLinkProps>) {
return ( return (
<button className={classes} tabIndex={0} onClick={(event) => handleLinks(event, to)}> <button className={classes} tabIndex={0} onClick={(event) => handleLinks(event, to)}>
{children} {children}
<IoArrowUp className={style.linkIcon} />
</button> </button>
); );
} }
@@ -1,11 +1,31 @@
export default function useElectronEvent() { import { useCallback, useEffect } from 'react';
const isElectron = window?.process?.type === 'renderer'; import { useNavigate } from 'react-router-dom';
const sendToElectron = (channel: string, args?: string | Record<string, unknown>) => { const isElectron = window.process?.type === 'renderer';
if (isElectron) { const ipcRenderer = isElectron ? window.require('electron').ipcRenderer : null;
window?.ipcRenderer.send(channel, args);
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 }; return { isElectron, sendToElectron };
} }
@@ -1,4 +1,5 @@
import { ChangeEvent, useRef, useState } from 'react'; import { ChangeEvent, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Button, Input } from '@chakra-ui/react'; import { Button, Input } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
@@ -13,14 +14,17 @@ import ProjectList from './ProjectList';
import style from './ProjectPanel.module.scss'; import style from './ProjectPanel.module.scss';
export default function ManageProjects() { export default function ManageProjects() {
const [isCreatingProject, setIsCreatingProject] = useState(false); const [searchParams, setSearchParams] = useSearchParams();
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState<'import' | null>(null); const [loading, setLoading] = useState<'import' | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const isCreatingProject = searchParams.get('create') === 'true';
const handleToggleCreate = () => { const handleToggleCreate = () => {
setIsCreatingProject((prev) => !prev); searchParams.set('create', isCreatingProject ? 'false' : 'true');
setSearchParams(searchParams);
}; };
const handleSelectFile = () => { const handleSelectFile = () => {
@@ -49,7 +53,8 @@ export default function ManageProjects() {
}; };
const handleCloseForm = () => { const handleCloseForm = () => {
setIsCreatingProject(false); searchParams.delete('create');
setSearchParams(searchParams);
}; };
return ( return (
+1 -1
View File
@@ -1,7 +1,7 @@
export const navigatorConstants = [ export const navigatorConstants = [
{ url: 'timer', label: 'Timer' }, { url: 'timer', label: 'Timer' },
{ url: 'clock', label: 'Clock' },
{ url: 'minimal', label: 'Minimal Timer' }, { url: 'minimal', label: 'Minimal Timer' },
{ url: 'clock', label: 'Wall Clock' },
{ url: 'backstage', label: 'Backstage' }, { url: 'backstage', label: 'Backstage' },
{ url: 'timeline', label: 'Timeline (beta)' }, { url: 'timeline', label: 'Timeline (beta)' },
{ url: 'public', label: 'Public' }, { url: 'public', label: 'Public' },
+3 -10
View File
@@ -10,7 +10,7 @@
"timer" "timer"
], ],
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "main.js", "main": "src/main.js",
"devDependencies": { "devDependencies": {
"electron": "^31.2.0", "electron": "^31.2.0",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
@@ -77,7 +77,7 @@
}, },
"files": [ "files": [
"**/*", "**/*",
"assets/", "src/assets/",
"!**/{yarn.lock,yarn-error.log}", "!**/{yarn.lock,yarn-error.log}",
"!**/{pnpm-lock.yaml}", "!**/{pnpm-lock.yaml}",
"!**/{test,tests,__test__,__tests__}", "!**/{test,tests,__test__,__tests__}",
@@ -85,7 +85,7 @@
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}" "!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
], ],
"directories": { "directories": {
"buildResources": "./assets/" "buildResources": "./src/assets/"
}, },
"extraResources": [ "extraResources": [
{ {
@@ -112,13 +112,6 @@
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}" "!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
] ]
}, },
{
"from": "../server/src/preloaded-db/",
"to": "extraResources/preloaded-db/",
"filter": [
"**/*"
]
},
{ {
"from": "../server/src/external/", "from": "../server/src/external/",
"to": "extraResources/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}`, production: (port = 4001) => `http://localhost:${port}`,
}, },
server: { server: {
pathToEntrypoint: '../extraResources/server/index.cjs', pathToEntrypoint: '../../extraResources/server/index.cjs',
}, },
assets: { assets: {
pathToAssets: './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 { app, BrowserWindow, Menu, globalShortcut, Tray, dialog, ipcMain, shell, Notification } = require('electron');
const path = require('path'); 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 { getApplicationMenu } = require('./menu/applicationMenu.js');
const isProduction = env === 'production'; const { getTrayMenu } = require('./menu/trayMenu.js');
const isMac = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
// path to server const electronConfig = require('./electron.config.js');
const nodePath = isProduction const {
? path.join(__dirname, electronConfig.server.pathToEntrypoint) env,
: path.join(__dirname, '../server/dist/index.cjs'); isProduction,
isWindows,
nodePath,
getClientUrl,
trayIcon,
appIcon,
getServerUrl,
} = require('./external.js');
if (!isProduction) { if (!isProduction) {
console.log(`Electron running in ${env} environment`); console.log(`Electron running in ${env} environment`);
@@ -20,10 +22,13 @@ if (!isProduction) {
process.traceProcessWarnings = true; process.traceProcessWarnings = true;
} }
// path to icons /** Flag holds server loading state */
const trayIcon = path.join(__dirname, electronConfig.assets.pathToAssets, 'background.png'); let loaded = 'Ontime running';
const appIcon = path.join(__dirname, electronConfig.assets.pathToAssets, 'logo.png');
let loaded = 'Nothing loaded'; /**
* Flag whether user has requested a quit
* Used to coordinate window closes without exit
*/
let isQuitting = false; let isQuitting = false;
// initialise // initialise
@@ -56,13 +61,13 @@ async function startBackend() {
/** /**
* @description utility function to create a notification * @description utility function to create a notification
* @param title * @param {string} title - Notification title
* @param text * @param {string} body - Notification body
*/ */
function showNotification(title, text) { function showNotification(title, body) {
new Notification({ new Notification({
title, title,
body: text, body,
silent: true, silent: true,
}).show(); }).show();
} }
@@ -108,6 +113,14 @@ function escalateError(error) {
dialog.showErrorBox('An unrecoverable error occurred', 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 // Ensure there isn't another instance of the app running already
const lock = app.requestSingleInstanceLock(); const lock = app.requestSingleInstanceLock();
if (!lock) { if (!lock) {
@@ -141,7 +154,7 @@ function createWindow() {
skipTaskbar: true, skipTaskbar: true,
}); });
splash.setIgnoreMouseEvents(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); splash.loadURL(splashPath);
win = new BrowserWindow({ win = new BrowserWindow({
@@ -156,7 +169,7 @@ function createWindow() {
enableWebSQL: false, enableWebSQL: false,
darkTheme: true, darkTheme: true,
webPreferences: { webPreferences: {
preload: path.join(__dirname, './src/preload.js'), preload: path.join(__dirname, './preload.js'),
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false, contextIsolation: false,
}, },
@@ -173,18 +186,13 @@ app.whenReady().then(() => {
} }
createWindow(); createWindow();
startBackend() startBackend()
.then((port) => { .then((port) => {
// Load page served by node or use React dev run const clientUrl = getClientUrl(port);
const clientUrl = isProduction const serverUrl = getServerUrl(port);
? electronConfig.reactAppUrl.production(port) const menu = getApplicationMenu(askToQuit, clientUrl, serverUrl, redirectWindow, (url) =>
: electronConfig.reactAppUrl.development(port); win.webContents.downloadURL(url),
);
const template = getApplicationMenu(isMac, askToQuit, clientUrl, `v${version}`, (path) => {
win.loadURL(`${clientUrl}/${path}`);
});
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
win win
@@ -229,13 +237,9 @@ app.whenReady().then(() => {
} }
}); });
// create tray // create tray and set its context menu
tray = new Tray(trayIcon); tray = new Tray(trayIcon);
const trayContextMenu = getTrayMenu(bringToFront, askToQuit);
// Define context menu
const { getTrayMenu } = require('./src/menu/trayMenu.js');
const trayMenuTemplate = getTrayMenu(bringToFront, askToQuit);
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
tray.setContextMenu(trayContextMenu); tray.setContextMenu(trayContextMenu);
}); });
@@ -261,7 +265,7 @@ ipcMain.on('shutdown', () => {
/** /**
* Handles requests to set window properties * Handles requests to set window properties
*/ */
ipcMain.on('set-window', (event, arg) => { ipcMain.on('set-window', (_event, arg) => {
switch (arg) { switch (arg) {
case 'show-dev': case 'show-dev':
win.webContents.openDevTools({ mode: 'detach' }); win.webContents.openDevTools({ mode: 'detach' });
@@ -274,6 +278,10 @@ ipcMain.on('set-window', (event, arg) => {
/** /**
* Handles requests to open external links * Handles requests to open external links
*/ */
ipcMain.on('send-to-link', (event, arg) => { ipcMain.on('send-to-link', (_event, arg) => {
shell.openExternal(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 * Creates the application menu
* @param {boolean} isMac - Whether the target platform is mac
* @param {function} askToQuit - function for quitting process * @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) { function getApplicationMenu(askToQuit, clientUrl, serverUrl, redirectWindow, download) {
return [ const template = [
...(isMac ...(isMac ? [makeMacMenu(askToQuit)] : []),
? [ makeFileMenu(serverUrl, redirectWindow, download),
{ makeViewMenu(clientUrl),
label: 'Ontime', makeSettingsMenu(redirectWindow),
submenu: [ makeHelpMenu(redirectWindow),
{ role: 'about' }, ...(isProduction ? [] : [{ label: 'Dev', submenu: [{ role: 'toggleDevTools' }] }]),
{ 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/');
},
},
],
},
]; ];
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 }; 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} showApp - function for making the window visible
* @param {function} askToQuit - function for quitting process * @param {function} askToQuit - function for quitting process
* @returns {Menu} - application tray menu
*/ */
function getTrayMenu(showApp, askToQuit) { function getTrayMenu(showApp, askToQuit) {
return [ return Menu.buildFromTemplate([
{ {
label: 'Show App', label: 'Show App',
click: () => showApp(), click: showApp,
}, },
{ {
label: 'Shutdown', label: 'Shutdown',
click: () => askToQuit(), click: askToQuit,
}, },
]; ]);
} }
module.exports = { getTrayMenu }; module.exports = { getTrayMenu };
+1 -1
View File
@@ -91,7 +91,7 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<img src="../../assets/logo.png" /> <img src="../assets/logo.png" />
<h1>ontime · event timers</h1> <h1>ontime · event timers</h1>
<div class="lds-ellipsis"> <div class="lds-ellipsis">
<div></div> <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 * Allows downloading of project files
*/ */
+2
View File
@@ -2,6 +2,7 @@ import express from 'express';
import { import {
createProjectFile, createProjectFile,
currentProjectDownload,
deleteProjectFile, deleteProjectFile,
duplicateProjectFile, duplicateProjectFile,
getInfo, getInfo,
@@ -23,6 +24,7 @@ import {
export const router = express.Router(); export const router = express.Router();
router.get('/', currentProjectDownload);
router.post('/download', validateFilenameBody, projectDownload); router.post('/download', validateFilenameBody, projectDownload);
router.post('/upload', uploadProjectFile, postProjectFile); router.post('/upload', uploadProjectFile, postProjectFile);
@@ -51,6 +51,13 @@ function init() {
ensureDirectory(resolveCorruptDirectory); ensureDirectory(resolveCorruptDirectory);
} }
export async function getCurrentProject() {
const filename = await getLastLoadedProject();
const pathToFile = getPathToProject(filename);
return { filename, pathToFile };
}
/** /**
* Private function loads a demo project * Private function loads a demo project
* to be composed in the loading functions * 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.goto('http://localhost:4001/');
await page.locator('data-test-id=timer-view'); 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.getByRole('button', { name: 'toggle menu' }).click();
await page.locator('data-test-id=navigation__menu'); await page.locator('data-test-id=navigation__menu');
await page.getByRole('link', { name: 'Minimal Timer' }).click(); await page.getByRole('link', { name: 'Minimal Timer' }).click();
await page.locator('data-test-id=minimal-timer'); await page.locator('data-test-id=minimal-timer');
await expect(page).toHaveURL('http://localhost:4001/minimal'); 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.getByRole('button', { name: 'toggle menu' }).click();
await page.locator('data-test-id=navigation__menu'); await page.locator('data-test-id=navigation__menu');
await page.getByRole('link', { name: 'Backstage' }).click(); 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 page.locator('data-test-id=timer-view');
await expect(page).toHaveURL('http://localhost:4001/timer'); await expect(page).toHaveURL('http://localhost:4001/timer');
}); });
}); });