Files
Tooloop-Kiosk-Browser/main.js
T
2024-06-03 12:24:44 +02:00

279 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { app, BrowserWindow, BaseWindow, nativeTheme, Menu, MenuItem } = require('electron/main');
const path = require('node:path');
const fs = require('fs');
const os = require('os');
const log = require('electron-log/main');
const isReachable = require('is-reachable');
const { setInterval } = require('node:timers/promises');
const configModule = require('./js/config.js');
const isMac = process.platform === 'darwin';
//------------------------------------------------------------------------------
// Properties
//------------------------------------------------------------------------------
let config;
let configWindow;
let win;
let menu;
//------------------------------------------------------------------------------
// Function
//------------------------------------------------------------------------------
/**
* Loads a config file from disc.
* The file is expected to be named `config.json` and is searched for in these
* locations and in this order:
*
* 1. `/assets/presentation/config.json` (Tooloop OS)
* 2. Path of the executable
* - Linux: `app.getPath('exe')`
* - MacOS: `path.resolve(app.getPath('exe'), "../../../../")`
* 3. `__dirname` (Development)
*/
function loadConfig() {
const locations = [
'/assets/presentation',
isMac ? path.resolve(app.getPath('exe'), "../../../../") : app.getPath('exe'),
__dirname
];
let filePath;
// Check all locations
for (const location of locations) {
// Update the filepath
filePath = path.join(location, 'config.json');
try {
// Try access
fs.accessSync(filePath);
// Parse the file if found
console.info('Found config file at ' + filePath);
const data = fs.readFileSync(filePath, { encoding: 'utf8' });
config = JSON.parse(data);
// Break the loop
break;
} catch (err) {
console.warn('No config file found at ' + filePath);
}
}
}
/**
* Creates the browser window
*/
function createMainWindow() {
win = new BrowserWindow({
width: 1920,
height: 1080,
backgroundColor: '#000000',
icon: 'images/icon-512.png',
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, './js/preload.js'),
webSecurity: false,
disableDialogs: true
},
});
nativeTheme.themeSource = 'dark';
// register event callbacks
win.on("closed", function () { win = null; });
win.webContents.on("will-frame-navigate", (event) => validateDomain(event));
win.webContents.setWindowOpenHandler(({ url }) => {
// we need to manually validate the url as `loadURL`
// doesnt trigger the `will-navigate` event
let event = new Event("DummyNavigation");
event.url = url;
if (validateDomain(event)) {
win.loadURL(url);
}
return { action: 'deny' };
});
win.on("page-title-updated", (event) => event.preventDefault());
// show initial black window
// win.setKiosk(true);
win.show();
// Load page from config file
if (config != undefined && 'url' in config) {
loadUrlAsync(config.url);
} else {
try {
// Load file from data folder if available
fs.accessSync('/assets/data/index.html');
win.loadFile('/assets/data/index.html');
} catch {
// Load fallback page
win.loadFile('./html/onboarding.html');
}
}
}
/**
* Creates a modal config view and attaches as to the main window
*/
function createConfigWindow() {
configWindow = new BrowserWindow({
parent: win,
modal: true,
width: 640,
height: 460,
show: false,
backgroundColor: '#1f1f1f'
});
configWindow.loadFile('./html/config.html');
}
/**
* Creates all available keyboard shortcuts
*/
function createMenu() {
menu = new Menu();
menu.append(new MenuItem({
label: app.name,
submenu: [
{
label: 'Toggle Fullscreen',
accelerator: isMac ? 'Cmd+F' : 'Ctrl+F',
click: () => { win.setKiosk(!win.kiosk); }
},
{ role: 'reload' },
{ role: 'quit' },
{
label: 'Config',
accelerator: isMac ? 'Cmd+,' : 'Ctrl+,',
click: () => { configWindow.isVisible() ? configWindow.hide() : configWindow.show(); }
}
]
}));
Menu.setApplicationMenu(menu);
}
/**
* Validates the url of a navigatio event against the list of allowed domains in
* the config file. See https://www.electronjs.org/docs/latest/api/web-contents#event-will-frame-navigate
* @param {Event} event
* @returns `true` if the url is allowed, `false` otherwise
*/
function validateDomain(event) {
if (config == undefined) return true;
let url = new URL(event.url);
// allow local urls
if (['file:', 'file'].includes(url.protocol)) return true;
if ('allowedDomains' in config && !config.allowedDomains.includes(url.hostname)) {
event.preventDefault();
log.info("Navigation to " + event.url + " prevented");
return false;
}
return true;
}
/**
* Tests if the host of the url is reachable.
* Access is tried in a 1 second interval and the url is loaded if successfull.
* @param {string} testurl
*/
async function loadUrlAsync(testurl) {
try {
let url = new URL(testurl);
// If its an online url, test whether its reachable
if (['https:', 'http:', 'https', 'http'].includes(url.protocol)) {
reachable = await isReachable(url.href);
if (reachable) {
log.info("Successfull access to " + url.hostname);
log.info("Loading " + config.url);
win.loadURL(config.url);
} else {
log.warn("Could not reach " + url.hostname);
for await (_ of setInterval(1000)) {
reachable = await isReachable(url.href);
if (reachable) {
log.info("Successfull access to " + url.hostname);
log.info("Loading " + config.url);
win.loadURL(config.url);
// break the interval
break;
} else {
log.warn("Could not reach " + url.hostname);
}
}
}
}
// Load other protocols (i. e. offline) immedately
else {
win.loadURL(config.url);
}
} catch (err) {
log.error(err);
}
}
//------------------------------------------------------------------------------
// Init electron app
//------------------------------------------------------------------------------
// https://github.com/electron/electron/issues/17972
app.commandLine.appendSwitch('--no-sandbox');
app.commandLine.appendSwitch('no-sandbox');
app.whenReady().then(() => {
// configModule.load();
console.log(configModule.config);
loadConfig();
log.initialize();
if (config != undefined && 'logPath' in config) {
log.transports.file.resolvePathFn = () => config.logPath;
}
log.info('----------------------------------------------');
log.info('Starting Tooloop Kiosk Browser...');
createMenu();
createMainWindow();
createConfigWindow()
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
})
app.on('window-all-closed', () => {
app.quit()
});
app.on('quit', () => {
log.info('Quit');
});
// prevent error dialogs in case of exceptions
process.on('uncaughtException', function (error) {
log.error(error);
});