V2 monorepo (#285)

* refactor(project structure): UI

* refactor(project structure): extract utilities

* refactor(project structure): remove unused

* refactor(project structure): electron

* refactor(project structure): server

refactor: migrate to vitest

refactor: monorepo config

* refactor: extract application menu

* refactor: exit process

* refactor: extract tray menu

* chore: electron build

* Added Seconds in studio clock #282
---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>

---------

Co-authored-by: Fabian Posenau <fabian.p99@gmx.de>
Co-authored-by: Fabian Posenau <fabian@fphome.de>
This commit is contained in:
Carlos Valente
2023-02-14 22:02:15 +01:00
committed by GitHub
parent 3918758d32
commit de9a7a87fd
439 changed files with 11381 additions and 14294 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules
dist
assets
+13
View File
@@ -0,0 +1,13 @@
{
"parserOptions": {
"sourceType": "module"
},
"env": {
"node": true
},
"extends": [
"eslint:recommended"
],
"plugins": [],
"rules": {}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+21
View File
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="275" height="275" fill="none" viewBox="0 0 275 275">
<path fill="#4D4D4D" d="M49.83 133.919c0-18.343 3.531-34.692 10.595-49.048 7.064-14.355 17.204-25.464 30.42-33.325 13.33-7.861 28.768-11.792 46.313-11.792 24.952 0 45.288 7.633 61.011 22.9 15.837 15.267 24.666 36.003 26.489 62.207l.342 12.647c0 28.369-7.918 51.155-23.755 68.359-15.836 17.09-37.085 25.635-63.745 25.635-26.66 0-47.965-8.545-63.916-25.635-15.836-17.09-23.755-40.332-23.755-69.726v-2.222zm49.389 3.589c0 17.545 3.304 30.989 9.912 40.332 6.608 9.228 16.064 13.843 28.369 13.843 11.963 0 21.305-4.558 28.028-13.672 6.722-9.229 10.083-23.926 10.083-44.092 0-17.204-3.361-30.534-10.083-39.99-6.723-9.457-16.179-14.185-28.37-14.185-12.076 0-21.419 4.728-28.027 14.185-6.608 9.342-9.912 23.869-9.912 43.579z"/>
<mask id="a" width="177" height="193" x="49" y="39" maskUnits="userSpaceOnUse">
<path fill="#fff" d="M49.83 133.919c0-18.343 3.531-34.692 10.595-49.048 7.064-14.355 17.204-25.464 30.42-33.325 13.33-7.861 28.768-11.792 46.313-11.792 24.952 0 45.288 7.633 61.011 22.9 15.837 15.267 24.666 36.003 26.489 62.207l.342 12.647c0 28.369-7.918 51.155-23.755 68.359-15.836 17.09-37.085 25.635-63.745 25.635-26.66 0-47.965-8.545-63.916-25.635-15.836-17.09-23.755-40.332-23.755-69.726v-2.222zm49.389 3.589c0 17.545 3.304 30.989 9.912 40.332 6.608 9.228 16.064 13.843 28.369 13.843 11.963 0 21.305-4.558 28.028-13.672 6.722-9.229 10.083-23.926 10.083-44.092 0-17.204-3.361-30.534-10.083-39.99-6.723-9.457-16.179-14.185-28.37-14.185-12.076 0-21.419 4.728-28.027 14.185-6.608 9.342-9.912 23.869-9.912 43.579z"/>
</mask>
<g mask="url(#a)">
<path fill="url(#paint0_linear)" d="M19.07 95.347c15.556-6.524 14.288-34.989 22.08-30.11 9.74 6.099 0-23.084 13.549-16.56 21.714 10.455-10.977 18.645 12.296 32.007 23.274 13.362 117.508 24.346 150.898 90.045 12.812 25.21 19.341 32.653 21.836 34.543 1.508-.13 1.78 1.349 0 0-.38.033-.838.168-1.362.476-5.516 3.245-106.876 85.867-148.54 34.124C48.162 188.129 2.25 140.221 14.31 127.463c9.648-10.205 1.393-25.103 4.76-32.116z"/>
<path fill="url(#paint1_linear)" d="M42.153 69.754c-7.791-4.88-.412 13.939-24.087 22.582-3.366 7.013 4.63 19.904-5.018 30.109-12.06 12.757 37.623 69.197 79.288 120.94 41.664 51.743 123.453-24.356 128.969-27.6 5.515-3.245 14.092 10.618-10.114-37.154-33.382-65.882-126.481-75.197-149.59-98.582-20.018-20.26 33.92-42.547 10.662-37.896-30.11 6.022-20.37 33.7-30.11 27.6z"/>
</g>
<defs>
<linearGradient id="paint0_linear" x1="93.96" x2="67.62" y1="-33.993" y2="351.251" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-color="#414141" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="88.433" x2="62.092" y1="-37.321" y2="347.924" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF005C" stop-opacity=".74"/>
<stop offset="0"/>
<stop offset="1" stop-color="#242424" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

+18
View File
@@ -0,0 +1,18 @@
module.exports = {
appIni: {
shutdownCode: 99,
},
reactAppUrl: {
development: 'http://localhost:3000/editor',
production: 'http://localhost:4001/editor',
},
externalUrls: {
help: 'https://cpvalente.gitbook.io/ontime/',
},
server: {
pathToEntrypoint: '../extraResources/server/index.cjs'
},
assets: {
pathToAssets: './assets/',
}
};
+272
View File
@@ -0,0 +1,272 @@
const {
app,
BrowserWindow,
Menu,
globalShortcut,
Tray,
dialog,
ipcMain,
shell,
Notification,
} = require('electron');
const path = require('path');
const electronConfig = require('./electron.config');
const env = process.env.NODE_ENV || 'production';
const isProduction = env === 'production';
const isMac = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
// path to server
const nodePath = isProduction
? path.join(__dirname, electronConfig.server.pathToEntrypoint)
: path.join(__dirname, '../server/dist/index.cjs');
if (!isProduction) {
console.log(`Electron running in ${env} environment`);
console.log(`Ontime server at ${nodePath}`);
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';
let isQuitting = false;
// initialise
let win;
let splash;
let tray = null;
(async () => {
// in dev mode, we expect both UI and server to be running
if (!isProduction) {
return
}
try {
const ontimeServer = require(nodePath)
const { startServer, startOSCServer } = ontimeServer;
// Start express server
loaded = await startServer();
// Start OSC Server
await startOSCServer();
} catch (error) {
loaded = error;
}
})();
/**
* @description utility function to create a notification
* @param title
* @param text
*/
function showNotification(title, text) {
new Notification({
title,
body: text,
silent: true,
}).show();
}
function appShutdown() {
// terminate node service
(async () => {
console.log('asking for shutdown 1', nodePath)
const ontimeServer = require(nodePath)
console.log('asking for shutdown 2', ontimeServer)
const { shutdown } = ontimeServer;
await shutdown(electronConfig.appIni.shutdownCode);
})();
isQuitting = true;
tray.destroy();
win.destroy();
app.quit();
}
function bringToFront() {
win.show();
win.focus();
}
function askToQuit() {
bringToFront();
win.send('user-request-shutdown');
}
// Ensure there isn't another instance of the app running already
const lock = app.requestSingleInstanceLock();
if (!lock) {
dialog.showErrorBox('Multiple instances', 'An instance of the App is already running.');
app.quit();
} else {
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.
if (win) {
if (win.isMinimized()) {
win.restore();
}
bringToFront();
}
});
}
function createWindow() {
splash = new BrowserWindow({
width: 333,
height: 333,
transparent: true,
icon: appIcon,
resizable: false,
frame: false,
alwaysOnTop: true,
focusable: false,
skipTaskbar: true,
});
splash.setIgnoreMouseEvents(true);
splash.loadURL(`file://${__dirname}/src/splash/splash.html`);
win = new BrowserWindow({
width: 1920,
height: 1000,
minWidth: 525,
minHeight: 405,
maxWidth: 1920,
maxHeight: 1440,
backgroundColor: '#101010', // $gray-1350
icon: appIcon,
show: false,
textAreasAreResizable: false,
enableWebSQL: false,
darkTheme: true,
webPreferences: {
preload: path.join(__dirname, './src/preload.js'),
nodeIntegration: true,
contextIsolation: false,
},
});
win.setMenu(null);
}
app.disableHardwareAcceleration();
app.whenReady().then(() => {
// Set app title in windows
if (isWindows) {
app.setAppUserModelId(app.name);
}
createWindow();
// register global shortcuts
// (available regardless of whether app is in focus)
// bring focus to window
globalShortcut.register('Alt+1', () => {
bringToFront();
});
// cheat to schedule process
setTimeout(() => {
// Load page served by node or use React dev run
const clientUrl = isProduction
? electronConfig.reactAppUrl.production
: electronConfig.reactAppUrl.development;
win.loadURL(clientUrl).then(() => {
win.webContents.setBackgroundThrottling(false);
win.show();
win.focus();
splash.destroy();
if (typeof loaded === 'string') {
tray.setToolTip(loaded);
} else {
tray.setToolTip('Initialising error: please restart Ontime');
}
});
}, 0);
// recreate window if no others open
app.on('activate', () => {
win.show();
});
// Hide on close
win.on('close', function (event) {
event.preventDefault();
if (!isQuitting) {
showNotification('Window Closed', 'App running in background');
win.hide();
}
});
// create tray
tray = new Tray(trayIcon);
// Define context menu
const { getTrayMenu } = require('./src/menu/trayMenu.js');
const trayMenuTemplate = getTrayMenu(bringToFront, askToQuit)
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
tray.setContextMenu(trayContextMenu);
});
const { getApplicationMenu } = require('./src/menu/applicationMenu.js');
const template = getApplicationMenu(isMac, askToQuit)
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
// unregister shortcuts before quitting
app.once('will-quit', () => {
globalShortcut.unregisterAll();
});
// Get messages from react
// Test message
ipcMain.on('test-message', (event, arg) => {
showNotification('Test Message', 'test from react', arg);
});
// Ask for main window reload
// Test message
ipcMain.on('reload', () => {
win?.reload();
});
// Terminate
ipcMain.on('shutdown', () => {
console.log('Electron got IPC shutdown');
appShutdown();
});
// Window manipulation
ipcMain.on('set-window', (event, arg) => {
switch (arg) {
case 'to-max':
win.maximize();
break;
case 'to-tray':
win.maximize();
break;
case 'show-dev':
win.webContents.openDevTools({ mode: 'detach' });
break;
default:
console.log('Electron unhandled window request', arg)
}
});
// Open links external
ipcMain.on('send-to-link', (event, arg) => {
if (arg === 'help') {
shell.openExternal(electronConfig.externalUrls.help);
} else {
shell.openExternal(arg);
}
});
+101
View File
@@ -0,0 +1,101 @@
{
"name": "ontime",
"version": "2.0.0-alpha",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
"keywords": [
"lighdev",
"ontime",
"timer"
],
"license": "AGPL-3.0-only",
"main": "main.js",
"devDependencies": {
"electron": "^21.1.0",
"electron-builder": "^23.6.0",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"prettier": "^2.8.3"
},
"scripts": {
"postinstall": "",
"dev:electron": "NODE_ENV=development electron .",
"dist-win": "electron-builder --publish=never --x64 --win",
"dist-mac": "electron-builder --publish=never --x64 --mac",
"dist-linux": "electron-builder --publish=never --x64 --linux",
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"build": {
"productName": "ontime",
"appId": "no.lightdev.ontime",
"asar": true,
"dmg": {
"artifactName": "ontime-macOS.dmg",
"icon": "icon.icns"
},
"mac": {
"target": "dmg",
"category": "public.app-category.productivity",
"icon": "icon.icns"
},
"win": {
"target": "nsis"
},
"nsis": {
"artifactName": "ontime-win64.exe",
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"allowElevation": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"deleteAppDataOnUninstall": true,
"runAfterFinish": false,
"installerIcon": "icon.ico",
"installerSidebar": "ontime-install.bmp",
"uninstallerSidebar": "ontime-uninstall.bmp"
},
"linux": {
"target": "AppImage",
"artifactName": "ontime-linux.AppImage"
},
"files": [
"**/*",
"assets/",
"!**/{yarn.lock,yarn-error.log}",
"!**/{pnpm-lock.yaml}",
"!**/{test,tests,__test__,__tests__}",
"!**/{mock,mocks,__mock__,__mocks__}",
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
],
"directories": {
"buildResources": "./assets/"
},
"extraResources": [
{
"from": "../client/build",
"to": "extraResources/client/",
"filter": [
"**/*",
"!**/{yarn.lock,yarn-error.log}",
"!**/{pnpm-lock.yaml}",
"!**/{test,tests,__test__,__tests__}",
"!**/{mock,mocks,__mock__,__mocks__}",
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
]
},
{
"from": "../server/dist/",
"to": "extraResources/server/",
"filter": [
"**/*",
"!**/{yarn.lock,yarn-error.log}",
"!**/{pnpm-lock.yaml}",
"!**/{test,tests,__test__,__tests__}",
"!**/{mock,mocks,__mock__,__mocks__}",
"!*{.spec.js,*.test.js,*.spec.ts,.test.ts}"
]
}
]
}
}
+171
View File
@@ -0,0 +1,171 @@
const { shell } = require('electron');
/**
* Build description of application menu
* @param {boolean} isMac - Whether the target platform is mac
* @param {function} askToQuit - function for quitting process
*/
function getApplicationMenu(isMac, askToQuit) {
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: 'Timer',
accelerator: 'CmdOrCtrl+V',
click: async () => {
await shell.openExternal('http://localhost:4001/timer');
},
},
{
label: 'Clock',
click: async () => {
await shell.openExternal('http://localhost:4001/clock');
},
},
{
label: 'Minimal Timer',
click: async () => {
await shell.openExternal('http://localhost:4001/minimal');
},
},
{
label: 'Backstage',
click: async () => {
await shell.openExternal('http://localhost:4001/backstage');
},
},
{
label: 'Public',
click: async () => {
await shell.openExternal('http://localhost:4001/public');
},
},
{
label: 'Lower Thirds',
click: async () => {
await shell.openExternal('http://localhost:4001/lower');
},
},
{
label: 'PiP',
click: async () => {
await shell.openExternal('http://localhost:4001/pip');
},
},
{
label: 'Studio Clock',
click: async () => {
await shell.openExternal('http://localhost:4001/studio');
},
},
{
label: 'Countdown',
click: async () => {
await shell.openExternal('http://localhost:4001/countdown');
},
},
{ type: 'separator' },
{
label: 'Editor',
click: async () => {
await shell.openExternal('http://localhost:4001/editor');
},
},
{
label: 'Cuesheet',
click: async () => {
await shell.openExternal('http://localhost:4001/cuesheet');
},
},
],
},
{ type: 'separator' },
{ role: 'forceReload' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
],
},
{
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
...(isMac
? [{ type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' }]
: [{ role: 'close' }]),
],
},
{
role: 'help',
submenu: [
{
label: 'See on github',
click: async () => {
await shell.openExternal('https://github.com/cpvalente/ontime');
},
},
{
label: 'Online documentation',
click: async () => {
await shell.openExternal('https://cpvalente.gitbook.io/ontime/');
},
},
],
},
];
}
module.exports = { getApplicationMenu }
+19
View File
@@ -0,0 +1,19 @@
/**
* Build description of tray context menu
* @param {function} showApp - function for making the window visible
* @param {function} askToQuit - function for quitting process
*/
function getTrayMenu(showApp, askToQuit) {
return [
{
label: 'Show App (Alt + 1)',
click: () => showApp(),
},
{
label: 'Shutdown',
click: () => askToQuit(),
},
]
}
module.exports = { getTrayMenu }
+1
View File
@@ -0,0 +1 @@
window.ipcRenderer = require('electron').ipcRenderer;
+104
View File
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'unsafe-inline'; script-src 'self'"
/>
<title>ontime</title>
<style>
body {
-webkit-user-select: none;
user-select: none;
-webkit-app-region: drag;
animation: fadein 0s;
background-color: #0005;
}
h1 {
font-family: Roboto, sans-serif;
font-weight: 100;
color: #fffa;
font-size: 24px;
}
.container {
margin-top: 5vh;
display: grid;
place-items: center;
}
.lds-ellipsis {
display: inline-block;
position: relative;
width: 71px;
}
.lds-ellipsis div {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff7597aa;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 8px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 8px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 32px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 56px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
</style>
</head>
<body>
<div class="container">
<img src="../../assets/logo.png" />
<h1>ontime · event timers</h1>
<div class="lds-ellipsis">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</body>
</html>