- add gui electron application

! sometimes the tray icon does not open it's context menu
This commit is contained in:
Sören Oesterwind 2022-08-18 22:00:24 +02:00
parent 16a2549942
commit 013ad881d3
11 changed files with 2115 additions and 10 deletions

View File

@ -11,8 +11,10 @@ You can download complete binaries from the release tab.
## Development build
1. Download the repository
2. `npm install` to install dependecies
3. `node index.js` to launch
3. `npm start` to launch
### Startup Arguments
One can pass a `--headless` argument to start the server in headless mode. This will disable the GUI.
## Packaging
This is more a comment for the future version of me. There is one command for packaging using nexe.
```bash

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 97 KiB

222
electronAssets/index2.html Normal file
View File

@ -0,0 +1,222 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>openCountdown</title>
</head>
<body>
<style>
body,
html {
margin: 0;
padding: 0;
-webkit-app-region: drag;
}
body {
background-color: rgba(35, 35, 35, 1);
color: white;
font-family: 'Helvetica';
overflow: hidden;
}
#wrap {
width: 100%;
margin-top: 20px;
display: block;
text-align: center;
}
#logo {
width: 200px;
padding-top: 20px;
padding-bottom: 10px;
text-align: center;
user-select: none;
}
#actions {
padding: 10px;
display: block;
box-shadow: inset 0px 5px 10px rgba(0, 0, 0, 0.3);
padding-top: 20px;
padding-bottom: 20px;
background-color: #d40215;
background-image: url("background.svg");
background-position: 60% 60%;
user-select: none;
}
.dobutton {
font-size: 1em;
padding: 10px 20px;
border: 0px;
user-select: none;
cursor: pointer;
border-radius: 4px;
}
#launch {
background-color: rgba(255, 255, 255, 1);
margin-right: 5px;
border: 1px solid white;
}
#hide {
background-color: rgba(255, 255, 255, 0.1);
color: white;
margin-right: 5px;
border: 1px solid white;
}
#close {
background-color: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid white;
}
input {
-webkit-appearance: textfield;
background-color: white;
-webkit-rtl-ordering: logical;
cursor: text;
padding: 1px;
border-width: 2px;
border-style: inset;
border-color: initial;
border-image: initial;
}
#status,
#model {
user-select: none !important;
}
#status {
font-size: 40px;
font-weight: 100;
margin: 0;
color: rgba(255, 255, 255, 0.4);
}
#model {
font-size: 14px;
}
h1 {
font-size: 40px;
padding-top: 20px;
font-weight: 100;
}
#url {
padding-top: 5px;
padding-bottom: 10px;
-webkit-app-region: no-drag !important;
}
#meh {
background-color: #b00013;
background-image: url("background.svg");
background-position: 90% 90%;
margin-top: 30px;
padding-bottom: 10px;
}
textarea:focus,
input:focus {
outline: 0;
}
#ifs,
input[type='button'] {
-webkit-app-region: no-drag !important;
}
input {
-webkit-app-region: no-drag !important;
}
#ifp {
width: 50px;
text-align: center;
border-bottom-left-radius: 3px;
border-top-left-radius: 3px;
background-color: #d40215;
font-size: 13px;
color: #fff;
padding-top: 3px;
padding-bottom: 3px;
border: 1px solid white;
border-right-width: 1px;
}
#ifpb {
cursor: pointer;
border: 1px solid #ccc;
background-color: #b00013;
color: white;
border: 1px solid white;
padding: 3px 8px;
border-bottom-right-radius: 3px;
border-top-right-radius: 3px;
border-left-width: 0px;
margin-top: -1px;
font-size: 13px;
}
#ifpb:hover {
background-color: #666;
}
#ift {
color: white;
-webkit-appearance: checkbox;
cursor: pointer;
}
#ifs {
background-color: #d40215;
color: #fff;
border: 1px solid white;
font-size: 12px !important;
margin-bottom: -4px;
}
#guitext {
font-size: 12px;
color: white;
padding-top: 0px;
padding-bottom: 2px;
}
#bottombuttons {
padding-bottom: 100px;
}
</style>
<div id="wrap">
<div id="topwrap">
<img id="logo" src="../static/logo/logoProposal.svg" alt="openCountdown" />
<div id="model">model_text (version_etc)</div>
</div>
<div id="meh">
<h1 id="status">Status</h1>
<div id="url">URL</div>
</div>
<div id="actions">
<p><input type="text" maxlength="5" id="ifp" value="8000" /><input type="button" id="ifpb" value="Change" /></p>
<p>
<input type="checkbox" id="ift" />
<label for="ift" style="font-size: 12px">Start minimized</label>
</p>
<div id="bottombuttons">
<input type="button" class="dobutton" value="Launch GUI" id="launch" />
<input type="button" class="dobutton" value="Hide" id="hide" />
<input type="button" class="dobutton" value="Quit" id="close" />
</div>
</div>
</div>
<script type="text/javascript" src="window.js"></script>
</body>
</html>

33
electronAssets/window.js Normal file
View File

@ -0,0 +1,33 @@
document.getElementById('launch').addEventListener('click', function () {
api.send('skeleton-launch-gui')
})
document.getElementById('hide').addEventListener('click', function () {
api.send('skeleton-minimize')
})
document.getElementById('close').addEventListener('click', function () {
api.send('skeleton-close')
})
api.receive('info', function (info) {
document.getElementById('status').innerHTML = info.appStatus
document.getElementById('url').innerHTML = info.appURL
document.getElementById('model').innerHTML = `${info.appName}`
document.getElementById('ift').checked = info.startMinimised
document.getElementById('ifp').value = configObject.port
document.title = info.appName
})
api.send('info')
document.getElementById('ifpb').addEventListener('click', function () {
var e = document.getElementById('ifp')
api.send('skeleton-bind-port', e.value)
})
document.getElementById('ift').addEventListener('click', function () {
var e = document.getElementById('ift')
api.send('skeleton-start-minimised', e.checked)
})
api.send('skeleton-ready')

View File

@ -0,0 +1,12 @@
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('api', {
send: (channel, data) => {
// whitelist channels
ipcRenderer.send(channel, data)
},
receive: (channel, func) => {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
},
})

View File

@ -73,11 +73,12 @@ currentState = {
srvTime: 0,
enableColoredText: true,
debug: false,
sessionToken: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
sessionToken: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15),
};
let configObject = {
language: "en_uk"
language: "en_uk",
port: 3000
}
if(!fs.existsSync("config.json")) {
fs.writeFileSync("config.json", "{}");
@ -463,7 +464,7 @@ app.use(function (req, res, next) {
loggy.log("Starting server", "info", "Server");
const port = 3006;
const port = configObject.port;
process.on('SIGINT', function () {
loggy.log("Caught interrupt signal and shutting down gracefully", "info", "Shutdown");

View File

@ -1 +1 @@
[{"timestamp":"2022-08-18 16:11:01.607","level":"info","module":"Logging","message":"2022-08-18 16:11:01.607 [info] [Logging] Logging initialized"},{"timestamp":"2022-08-18 16:11:01.608","level":"info","module":"Server","message":"2022-08-18 16:11:01.608 [info] [Server] Preparing server"},{"timestamp":"2022-08-18 16:11:01.609","level":"info","module":"Server","message":"2022-08-18 16:11:01.609 [info] [Server] Preparing static routes"},{"timestamp":"2022-08-18 16:11:01.610","level":"info","module":"Server","message":"2022-08-18 16:11:01.610 [info] [Server] Preparing middlewares"},{"timestamp":"2022-08-18 16:11:01.611","level":"info","module":"Config","message":"2022-08-18 16:11:01.611 [info] [Config] Loading config"},{"timestamp":"2022-08-18 16:11:01.612","level":"info","module":"Language","message":"2022-08-18 16:11:01.612 [info] [Language] Searching for languages"},{"timestamp":"2022-08-18 16:11:01.612","level":"info","module":"Language","message":"2022-08-18 16:11:01.612 [info] [Language] Found 3 languages"},{"timestamp":"2022-08-18 16:11:01.612","level":"info","module":"Language","message":"2022-08-18 16:11:01.612 [info] [Language] Reading language file"},{"timestamp":"2022-08-18 16:11:01.612","level":"info","module":"Websocket","message":"2022-08-18 16:11:01.612 [info] [Websocket] Preparing websocket"},{"timestamp":"2022-08-18 16:11:01.613","level":"info","module":"Server","message":"2022-08-18 16:11:01.613 [info] [Server] Preparing routes"},{"timestamp":"2022-08-18 16:11:01.614","level":"info","module":"Server","message":"2022-08-18 16:11:01.614 [info] [Server] Starting server"},{"timestamp":"2022-08-18 16:13:27.222","level":"error","module":"Security","message":"2022-08-18 16:13:27.222 [error] [Security] Attempt to access restricted asset file mdbootstrap/js/mdb.min.js"},{"timestamp":"2022-08-18 16:13:27.674","level":"error","module":"Security","message":"2022-08-18 16:13:27.674 [error] [Security] Attempt to access restricted asset file mdbootstrap/js/mdb.min.js"},{"timestamp":"2022-08-18 16:13:46.931","level":"info","module":"Shutdown","message":"2022-08-18 16:13:46.931 [info] [Shutdown] Caught interrupt signal and shutting down gracefully"}]
[{"timestamp":"2022-08-18 16:19:06.649","level":"info","module":"Logging","message":"2022-08-18 16:19:06.649 [info] [Logging] Logging initialized"},{"timestamp":"2022-08-18 16:19:06.651","level":"info","module":"Server","message":"2022-08-18 16:19:06.651 [info] [Server] Preparing server"},{"timestamp":"2022-08-18 16:19:06.652","level":"info","module":"Server","message":"2022-08-18 16:19:06.652 [info] [Server] Preparing static routes"},{"timestamp":"2022-08-18 16:19:06.654","level":"info","module":"Server","message":"2022-08-18 16:19:06.654 [info] [Server] Preparing middlewares"},{"timestamp":"2022-08-18 16:19:06.655","level":"info","module":"Config","message":"2022-08-18 16:19:06.655 [info] [Config] Loading config"},{"timestamp":"2022-08-18 16:19:06.658","level":"info","module":"Language","message":"2022-08-18 16:19:06.658 [info] [Language] Searching for languages"},{"timestamp":"2022-08-18 16:19:06.659","level":"info","module":"Language","message":"2022-08-18 16:19:06.659 [info] [Language] Found 3 languages"},{"timestamp":"2022-08-18 16:19:06.659","level":"info","module":"Language","message":"2022-08-18 16:19:06.659 [info] [Language] Reading language file"},{"timestamp":"2022-08-18 16:19:06.675","level":"info","module":"Websocket","message":"2022-08-18 16:19:06.675 [info] [Websocket] Preparing websocket"},{"timestamp":"2022-08-18 16:19:06.675","level":"info","module":"Server","message":"2022-08-18 16:19:06.675 [info] [Server] Preparing routes"},{"timestamp":"2022-08-18 16:19:06.678","level":"info","module":"Server","message":"2022-08-18 16:19:06.678 [info] [Server] Starting server"},{"timestamp":"2022-08-18 16:22:57.338","level":"info","module":"Shutdown","message":"2022-08-18 16:22:57.338 [info] [Shutdown] Caught interrupt signal and shutting down gracefully"}]

191
newStartHandler.js Normal file
View File

@ -0,0 +1,191 @@
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, MenuItem, dialog } = require('electron')
const path = require('path')
const fs = require('fs')
const _ = require("underscore")
const open = require('open');
const childProcess = require('child_process');
const packageJson = JSON.parse(fs.readFileSync("package.json"))
// a minimal config
let configObject = {
language: "en_uk",
startMinimised: false,
port: 3000
}
if (!fs.existsSync("config.json")) {
fs.writeFileSync("config.json", "{}");
}
const tempJsonText = JSON.parse(fs.readFileSync("config.json", "utf8"));
configObject = _.extend(configObject, tempJsonText);
fs.writeFileSync("config.json", JSON.stringify(configObject));
const processArgs = process.argv;
// Check if --headless is passed as an argument
if (processArgs.includes("--headless")) {
startServer()
} else {
// Start electron
app.whenReady().then(() => {
startServer()
createTray()
createWindow()
})
ipcMain.on('info', function () {
if (win) {
win.webContents.send('info', { "appStatus": "Testing", "appURL": "http://127.0.0.1:" + configObject.port, "appName": "openCountdown " + packageJson.version, "startMinimised": configObject.startMinimised, "port": configObject.port })
}
})
ipcMain.on('skeleton-close', function (req, cb) {
trayQuit()
})
ipcMain.on('skeleton-minimize', function (req, cb) {
win.hide()
})
ipcMain.on('skeleton-launch-gui', function () {
launchUI()
})
ipcMain.on('skeleton-start-minimised', function (e, msg) {
configObject.startMinimised = msg
fs.writeFileSync("config.json", JSON.stringify(configObject));
})
ipcMain.on('skeleton-bind-port', function (e, msg) {
configObject.port = msg
console.log("Update port")
fs.writeFileSync("config.json", JSON.stringify(configObject));
dialog.showMessageBoxSync({
message: "Port changed. Restart openCountdown to apply change.",
buttons: ["OK"]})
})
}
var win, tray = null;
const createWindow = () => {
win = new BrowserWindow({
width: 370,
height: 500,
transparent: true,
frame: false,
resizable: false,
icon: path.join(__dirname, 'static/logo/faviconLogo.svg'),
webPreferences: {
pageVisibility: true,
nodeIntegration: true,
contextIsolation: true,
preload: path.join(__dirname, 'electronAssets/windowPreload.js'),
}
})
win.loadFile('electronAssets/index2.html')
if (configObject.startMinimised) {
win.hide()
}
}
function createTray() {
tray = new Tray('static/logo/faviconLogo.png')
tray.setIgnoreDoubleClickEvents(true)
const menu = new Menu()
menu.append(
new MenuItem({
label: 'Show window',
click: showScreen,
})
)
menu.append(
new MenuItem({
label: 'Launch GUI',
click: launchUI,
})
)
menu.append(
new MenuItem({
label: 'Quit',
accelerator: 'Command+Q',
click: trayQuit,
})
)
tray.setToolTip('openCountdown ' + packageJson.version)
tray.setContextMenu(menu)
tray.on('click', function (e) {
if (win.isVisible()) {
win.hide()
} else {
win.show()
}
});
}
function showScreen(){
win.show()
}
function launchUI(){
open("http://127.0.0.1:" + configObject.port)
}
function trayQuit(){
let options = {
buttons: ["Yes","No"],
message: "Do you really want to quit openCountdown?"
}
let response = dialog.showMessageBoxSync(options)
if(response == 0){
srvProc.kill("SIGINT")
setTimeout(app.quit, 1000)
}
}
// taken from https://stackoverflow.com/a/22649812/11317151
function runScript(scriptPath, callback, valueCb) {
// keep track of whether callback has been invoked to prevent multiple invocations
var invoked = false;
var process = childProcess.fork(scriptPath);
valueCb(process)
// listen for errors as they may prevent the exit event from firing
process.on('error', function (err) {
if (invoked) return;
invoked = true;
callback(err);
});
// execute the callback once the process has finished running
process.on('exit', function (code) {
if (invoked) return;
invoked = true;
// var err = code === 0 ? null : new Error('exit code ' + code);
callback(code);
});
}
srvProc = null;
function setServer(process){
srvProc = process
}
function startServer(){
runScript(path.join(__dirname, 'index.js'), function (err) {
if (err) throw err;
}, setServer)
}

1643
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,12 @@
{
"name": "opencountdown",
"version": "1.0.2",
"version": "1.0.3",
"description": "An opensource countdown",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "nexe index.js --build"
"build": "nexe index.js --build",
"start": "electron newStartHandler.js"
},
"author": "TheGreydiamond",
"license": "LGPL-3.0",
@ -23,7 +24,11 @@
"js-cookie": "^3.0.1",
"less": "^3.13",
"mdbootstrap": "^4.20.0",
"open": "^8.4.0",
"underscore": "^1.13.3",
"ws": "^8.5.0"
},
"devDependencies": {
"electron": "^20.0.3"
}
}

BIN
static/logo/faviconLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB