1339 lines
36 KiB
JavaScript
1339 lines
36 KiB
JavaScript
const path = require("path");
|
|
const axios = require("axios");
|
|
const https = require("https");
|
|
const hardware = require("./hardware");
|
|
const integration = require("./integration");
|
|
const {
|
|
app,
|
|
nativeTheme,
|
|
globalShortcut,
|
|
ipcMain,
|
|
dialog,
|
|
screen,
|
|
session,
|
|
BaseWindow,
|
|
WebContentsView,
|
|
} = require("electron");
|
|
|
|
global.WEBVIEW = global.WEBVIEW || {
|
|
initialized: false,
|
|
tracker: {
|
|
display: {},
|
|
pointer: {
|
|
position: {},
|
|
time: new Date(),
|
|
},
|
|
window: {
|
|
status: null,
|
|
},
|
|
widget: {
|
|
focused: null,
|
|
},
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Initializes the webview with the provided arguments.
|
|
*
|
|
* @returns {bool} Returns true if the initialization was successful.
|
|
*/
|
|
const init = async () => {
|
|
if (ARGS.web_url.length === 0) {
|
|
console.error("Please provide the '--web-url' parameter");
|
|
return app.quit();
|
|
}
|
|
if (ARGS.web_url.some((url) => !/^https?:\/\//.test(url))) {
|
|
console.error("Please provide the '--web-url' parameter with http(s)");
|
|
return app.quit();
|
|
}
|
|
session.defaultSession.clearCache();
|
|
|
|
// Parse arguments
|
|
const debug = "app_debug" in ARGS;
|
|
const widget = ARGS.web_widget ? ARGS.web_widget === "true" : true;
|
|
const zoom = !isNaN(parseFloat(ARGS.web_zoom)) ? parseFloat(ARGS.web_zoom) : 1.25;
|
|
const theme = ["light", "dark"].includes(ARGS.web_theme) ? ARGS.web_theme : "dark";
|
|
const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url);
|
|
|
|
// Init global properties
|
|
WEBVIEW.viewActive = 0;
|
|
WEBVIEW.viewUrls = urls;
|
|
WEBVIEW.viewZoom = zoom;
|
|
WEBVIEW.statusEnabled = !debug;
|
|
WEBVIEW.pagerEnabled = widget;
|
|
WEBVIEW.widgetEnabled = widget;
|
|
WEBVIEW.navigationEnabled = widget;
|
|
|
|
// Init global display
|
|
const display = screen.getPrimaryDisplay();
|
|
WEBVIEW.display = display ? display.workAreaSize : {};
|
|
if (!WEBVIEW.display.width || !WEBVIEW.display.height) {
|
|
WEBVIEW.display = { width: 800, height: 600 };
|
|
}
|
|
|
|
// Init global theme
|
|
WEBVIEW.theme = {
|
|
set: (name) => {
|
|
nativeTheme.themeSource = name.toLowerCase();
|
|
},
|
|
get: () => {
|
|
return nativeTheme.shouldUseDarkColors ? "dark" : "light";
|
|
},
|
|
toggle: () => {
|
|
nativeTheme.themeSource = nativeTheme.shouldUseDarkColors ? "light" : "dark";
|
|
},
|
|
handler: (callback) => {
|
|
nativeTheme.on("updated", callback);
|
|
},
|
|
};
|
|
WEBVIEW.theme.handler(updateTheme);
|
|
WEBVIEW.theme.set(theme);
|
|
|
|
// Init global root window
|
|
WEBVIEW.window = new BaseWindow({
|
|
title: APP.title,
|
|
icon: APP.icon,
|
|
autoHideMenuBar: true,
|
|
frame: !WEBVIEW.statusEnabled,
|
|
width: Math.floor(WEBVIEW.display.width * 0.85),
|
|
height: Math.floor(WEBVIEW.display.height * 0.75),
|
|
minWidth: 136,
|
|
minHeight: 136,
|
|
});
|
|
const size = WEBVIEW.window.getBounds();
|
|
console.info(`Open Window: ${size.width}x${size.height}+${size.x}+${size.y}`);
|
|
updateTheme();
|
|
|
|
// Init global webview
|
|
WEBVIEW.views = [];
|
|
urls.forEach((url, i) => {
|
|
const view = new WebContentsView({
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
},
|
|
});
|
|
view.setVisible(i === 0);
|
|
view.setBackgroundColor("#FFFFFFFF");
|
|
WEBVIEW.window.contentView.addChildView(view);
|
|
WEBVIEW.views.push(view);
|
|
onlineStatus(url).then(() => {
|
|
view.webContents.loadURL(url);
|
|
});
|
|
});
|
|
|
|
// Init global pager
|
|
WEBVIEW.pager = new WebContentsView({
|
|
transparent: true,
|
|
webPreferences: {
|
|
nodeIntegration: true,
|
|
contextIsolation: false,
|
|
},
|
|
});
|
|
WEBVIEW.pager.setBackgroundColor("#00000000");
|
|
WEBVIEW.window.contentView.addChildView(WEBVIEW.pager);
|
|
WEBVIEW.pager.webContents.loadFile(path.join(APP.path, "html", "pager.html"));
|
|
|
|
// Init global widget
|
|
WEBVIEW.widget = new WebContentsView({
|
|
transparent: true,
|
|
webPreferences: {
|
|
nodeIntegration: true,
|
|
contextIsolation: false,
|
|
},
|
|
});
|
|
WEBVIEW.widget.setBackgroundColor("#00000000");
|
|
WEBVIEW.window.contentView.addChildView(WEBVIEW.widget);
|
|
WEBVIEW.widget.webContents.loadFile(path.join(APP.path, "html", "widget.html"));
|
|
|
|
// Init global status
|
|
WEBVIEW.status = new WebContentsView({
|
|
transparent: true,
|
|
webPreferences: {
|
|
nodeIntegration: true,
|
|
contextIsolation: false,
|
|
},
|
|
});
|
|
WEBVIEW.status.setBackgroundColor("#00000000");
|
|
WEBVIEW.window.contentView.addChildView(WEBVIEW.status);
|
|
WEBVIEW.status.webContents.loadFile(path.join(APP.path, "html", "status.html"));
|
|
|
|
// Init global navigation
|
|
WEBVIEW.navigation = new WebContentsView({
|
|
transparent: true,
|
|
webPreferences: {
|
|
nodeIntegration: true,
|
|
contextIsolation: false,
|
|
},
|
|
});
|
|
WEBVIEW.navigation.setBackgroundColor("#00000000");
|
|
WEBVIEW.window.contentView.addChildView(WEBVIEW.navigation);
|
|
WEBVIEW.navigation.webContents.loadFile(path.join(APP.path, "html", "navigation.html"));
|
|
|
|
// Register global events
|
|
await windowEvents();
|
|
await widgetEvents();
|
|
await statusEvents();
|
|
await navigationEvents();
|
|
await viewEvents();
|
|
await appEvents();
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Updates the shared webview properties.
|
|
*/
|
|
const update = async () => {
|
|
if (!WEBVIEW.initialized || APP.exiting) {
|
|
return;
|
|
}
|
|
console.debug("webview.js: update()");
|
|
|
|
// Update window status
|
|
updateStatus();
|
|
|
|
// Update pager status
|
|
updatePager();
|
|
|
|
// Update widget status
|
|
updateWidget();
|
|
|
|
// Update navigation status
|
|
updateNavigation();
|
|
|
|
// Update integration sensor
|
|
integration.update();
|
|
};
|
|
|
|
/**
|
|
* Updates the application theme.
|
|
*/
|
|
const updateTheme = () => {
|
|
if (APP.exiting) {
|
|
return;
|
|
}
|
|
const previous = WEBVIEW.statusTheme;
|
|
const theme = WEBVIEW.theme.get();
|
|
|
|
// Set controls theme
|
|
if (previous !== theme) {
|
|
console.info("Update Application Theme:", theme);
|
|
}
|
|
WEBVIEW.statusTheme = theme;
|
|
WEBVIEW.pagerTheme = theme;
|
|
WEBVIEW.widgetTheme = theme;
|
|
WEBVIEW.navigationTheme = theme;
|
|
|
|
// Update controls theme
|
|
WEBVIEW.window.setMenuBarVisibility(false);
|
|
if (WEBVIEW.initialized) {
|
|
resizeView();
|
|
}
|
|
EVENTS.emit("updateTheme");
|
|
};
|
|
|
|
/**
|
|
* Updates the active view.
|
|
*/
|
|
const updateView = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
const url = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL();
|
|
const host = url.startsWith("data:") ? "whoopsie" : new URL(url).host;
|
|
const title = `${APP.title} - ${host} (${WEBVIEW.viewActive})`;
|
|
const previous = WEBVIEW.window.getTitle();
|
|
|
|
// Update window title
|
|
if (previous !== title) {
|
|
console.info(`Update View: ${title}`);
|
|
}
|
|
WEBVIEW.status.webContents.send("text-content", { id: "title", content: title });
|
|
WEBVIEW.window.setTitle(title);
|
|
|
|
// Hide all other webviews and show only the active one
|
|
WEBVIEW.views.forEach((view, i) => {
|
|
view.setVisible(i === WEBVIEW.viewActive);
|
|
});
|
|
update();
|
|
};
|
|
|
|
/**
|
|
* Updates the window status.
|
|
*/
|
|
const updateStatus = () => {
|
|
const previous = WEBVIEW.tracker.window.status;
|
|
|
|
// Set window status
|
|
if (WEBVIEW.window.isFullScreen()) {
|
|
WEBVIEW.tracker.window.status = "Fullscreen";
|
|
} else if (WEBVIEW.window.isMinimized()) {
|
|
WEBVIEW.tracker.window.status = "Minimized";
|
|
} else if (WEBVIEW.window.isMaximized()) {
|
|
WEBVIEW.tracker.window.status = "Maximized";
|
|
} else {
|
|
WEBVIEW.tracker.window.status = "Framed";
|
|
}
|
|
|
|
// Update window status
|
|
if (previous !== WEBVIEW.tracker.window.status) {
|
|
console.info(`Update Kiosk Status: ${WEBVIEW.tracker.window.status}`);
|
|
}
|
|
EVENTS.emit("updateStatus");
|
|
};
|
|
|
|
/**
|
|
* Updates the pager control.
|
|
*/
|
|
const updatePager = () => {
|
|
// Disable pager buttons
|
|
WEBVIEW.pager.webContents.send("button-disabled", {
|
|
id: "previous",
|
|
disabled: WEBVIEW.viewActive <= 1,
|
|
});
|
|
WEBVIEW.pager.webContents.send("button-disabled", {
|
|
id: "next",
|
|
disabled: WEBVIEW.viewActive >= WEBVIEW.viewUrls.length - 1,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Updates the widget control.
|
|
*/
|
|
const updateWidget = () => {
|
|
// Hide keyboard button
|
|
WEBVIEW.widget.webContents.send("button-hidden", {
|
|
id: "keyboard",
|
|
hidden: !HARDWARE.support.keyboardVisibility,
|
|
});
|
|
|
|
// Hide navigation button
|
|
WEBVIEW.widget.webContents.send("button-hidden", {
|
|
id: "navigation",
|
|
hidden: !WEBVIEW.navigationEnabled,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Updates the navigation control.
|
|
*/
|
|
const updateNavigation = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
|
const currentUrl = view.webContents.getURL();
|
|
|
|
// Hide pager buttons
|
|
WEBVIEW.navigation.webContents.send("button-hidden", {
|
|
id: "previous",
|
|
hidden: WEBVIEW.viewUrls.length <= 2,
|
|
});
|
|
WEBVIEW.navigation.webContents.send("button-hidden", {
|
|
id: "next",
|
|
hidden: WEBVIEW.viewUrls.length <= 2,
|
|
});
|
|
|
|
// Disable pager buttons
|
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
|
id: "previous",
|
|
disabled: WEBVIEW.viewActive <= 1,
|
|
});
|
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
|
id: "next",
|
|
disabled: WEBVIEW.viewActive >= WEBVIEW.viewUrls.length - 1,
|
|
});
|
|
|
|
// Update url text
|
|
WEBVIEW.navigation.webContents.send("input-text", {
|
|
id: "url",
|
|
text: currentUrl.startsWith("data:") ? "" : currentUrl,
|
|
placeholder: defaultUrl.startsWith("data:") ? "" : defaultUrl,
|
|
});
|
|
WEBVIEW.navigation.webContents.send("input-readonly", {
|
|
id: "url",
|
|
readonly: !!HARDWARE.support.keyboardVisibility,
|
|
});
|
|
|
|
// Disable zoom buttons
|
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
|
id: "minus",
|
|
disabled: view.webContents.getZoomFactor().toFixed(2) <= 0.25,
|
|
});
|
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
|
id: "plus",
|
|
disabled: view.webContents.getZoomFactor().toFixed(2) >= 4.0,
|
|
});
|
|
|
|
// Disable history buttons
|
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
|
id: "backward",
|
|
disabled: !view.webContents.navigationHistory.canGoBack(),
|
|
});
|
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
|
id: "forward",
|
|
disabled: !view.webContents.navigationHistory.canGoForward(),
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Shows or hides the webview navigation bar.
|
|
*
|
|
* @param {string} force - Force the navigation bar visibility to 'ON' or 'OFF'.
|
|
*/
|
|
const toggleNavigation = (force = null) => {
|
|
if (!WEBVIEW.navigationEnabled) {
|
|
return;
|
|
}
|
|
const window = WEBVIEW.window.getBounds();
|
|
const status = WEBVIEW.status.getBounds();
|
|
const navigation = WEBVIEW.navigation.getBounds();
|
|
|
|
// Calculate navigation height
|
|
const height = force === "ON" ? 50 : force === "OFF" ? 0 : navigation.height > 0 ? 0 : 50;
|
|
if (height === navigation.height) {
|
|
return;
|
|
}
|
|
|
|
// Show or hide navigation based on height
|
|
WEBVIEW.navigation.setBounds({
|
|
x: 0,
|
|
y: window.height - status.height - height,
|
|
width: window.width,
|
|
height: height,
|
|
});
|
|
|
|
// Resize webview
|
|
resizeView();
|
|
};
|
|
|
|
/**
|
|
* Shows or hides the webview status bar.
|
|
*
|
|
* @param {string} force - Force the status bar visibility to 'ON' or 'OFF'.
|
|
*/
|
|
const toggleStatus = (force = null) => {
|
|
if (!WEBVIEW.statusEnabled) {
|
|
return;
|
|
}
|
|
const window = WEBVIEW.window.getBounds();
|
|
const status = WEBVIEW.status.getBounds();
|
|
|
|
// Calculate status height
|
|
const height = force === "ON" ? 40 : force === "OFF" ? 0 : status.height > 0 ? 0 : 40;
|
|
if (height === status.height) {
|
|
return;
|
|
}
|
|
|
|
// Show or hide status based on height
|
|
WEBVIEW.status.setBounds({
|
|
x: 0,
|
|
y: 0,
|
|
width: window.width,
|
|
height: height,
|
|
});
|
|
|
|
// Resize webview
|
|
resizeView();
|
|
};
|
|
|
|
/**
|
|
* Decreases page zoom on the active webview.
|
|
*/
|
|
const zoomMinus = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
|
const zoom = view.webContents.getZoomFactor() - 0.1;
|
|
view.webContents.setZoomFactor(Math.max(0.25, Math.round(zoom * 10) / 10));
|
|
update();
|
|
};
|
|
|
|
/**
|
|
* Increases page zoom on the active webview.
|
|
*/
|
|
const zoomPlus = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
|
const zoom = view.webContents.getZoomFactor() + 0.1;
|
|
view.webContents.setZoomFactor(Math.min(4.0, Math.round(zoom * 10) / 10));
|
|
update();
|
|
};
|
|
|
|
/**
|
|
* Navigates backward in the history of the active webview.
|
|
*/
|
|
const historyBackward = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
|
if (view.webContents.navigationHistory.canGoBack()) {
|
|
view.webContents.navigationHistory.goBack();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Navigates forward in the history of the active webview.
|
|
*/
|
|
const historyForward = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
|
if (view.webContents.navigationHistory.canGoForward()) {
|
|
view.webContents.navigationHistory.goForward();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Activates the previous webview page.
|
|
*/
|
|
const previousView = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
if (WEBVIEW.viewActive > 1) {
|
|
WEBVIEW.viewActive--;
|
|
}
|
|
updateView();
|
|
};
|
|
|
|
/**
|
|
* Activates the next webview page.
|
|
*/
|
|
const nextView = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
if (WEBVIEW.viewActive < WEBVIEW.views.length - 1) {
|
|
WEBVIEW.viewActive++;
|
|
}
|
|
updateView();
|
|
};
|
|
|
|
/**
|
|
* Reloads the default url and settings on the active webview.
|
|
*/
|
|
const homeView = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
|
const currentUrl = view.webContents.getURL();
|
|
|
|
// Reset log history
|
|
APP.logs = [];
|
|
|
|
// Reload the default url or refresh the page
|
|
if (currentUrl != defaultUrl) {
|
|
view.webContents.loadURL(defaultUrl);
|
|
} else {
|
|
view.webContents.reloadIgnoringCache();
|
|
}
|
|
|
|
// Reset page zoom and history
|
|
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
|
setTimeout(() => {
|
|
view.webContents.navigationHistory.clear();
|
|
update();
|
|
}, 2000);
|
|
};
|
|
|
|
/**
|
|
* Reloads the current url on the active webview.
|
|
*/
|
|
const reloadView = () => {
|
|
if (!WEBVIEW.viewActive) {
|
|
return;
|
|
}
|
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
|
const currentUrl = view.webContents.getURL();
|
|
|
|
// Reset log history
|
|
APP.logs = [];
|
|
|
|
// Reload the default url or refresh the page
|
|
if (currentUrl.startsWith("data:")) {
|
|
view.webContents.loadURL(defaultUrl);
|
|
} else {
|
|
view.webContents.reloadIgnoringCache();
|
|
}
|
|
update();
|
|
};
|
|
|
|
/**
|
|
* Resizes and positions all webviews.
|
|
*/
|
|
const resizeView = () => {
|
|
const window = WEBVIEW.window.getBounds();
|
|
const status = WEBVIEW.status.getBounds();
|
|
const navigation = WEBVIEW.navigation.getBounds();
|
|
const pager = { width: 20, height: window.height };
|
|
const widget = { width: 60, height: 200 };
|
|
|
|
// Update view size
|
|
WEBVIEW.views.forEach((view, i) => {
|
|
console.debug(`webview.js: resizeView(${i})`);
|
|
view.setBounds({
|
|
x: 0,
|
|
y: status.height,
|
|
width: window.width,
|
|
height: window.height - status.height - navigation.height,
|
|
});
|
|
});
|
|
|
|
// Update pager size
|
|
if (WEBVIEW.pagerEnabled) {
|
|
WEBVIEW.pager.setBounds({
|
|
x: window.width - pager.width,
|
|
y: status.height,
|
|
width: pager.width,
|
|
height: pager.height - status.height - navigation.height,
|
|
});
|
|
WEBVIEW.pager.webContents.send("data-theme", {
|
|
theme: WEBVIEW.viewUrls.length > 2 ? WEBVIEW.pagerTheme : "hidden",
|
|
});
|
|
}
|
|
|
|
// Update widget size
|
|
if (WEBVIEW.widgetEnabled) {
|
|
WEBVIEW.widget.setBounds({
|
|
x: WEBVIEW.tracker.widget.focused ? window.width - widget.width : window.width - pager.width,
|
|
y: status.height + parseInt((window.height - status.height - widget.height) / 2, 10),
|
|
width: widget.width,
|
|
height: widget.height,
|
|
});
|
|
WEBVIEW.widget.webContents.send("data-theme", {
|
|
theme: WEBVIEW.tracker.widget.focused ? WEBVIEW.widgetTheme : "hidden",
|
|
});
|
|
}
|
|
|
|
// Update status size
|
|
if (WEBVIEW.statusEnabled) {
|
|
WEBVIEW.status.setBounds({
|
|
x: 0,
|
|
y: 0,
|
|
width: window.width,
|
|
height: status.height,
|
|
});
|
|
WEBVIEW.status.webContents.send("data-theme", {
|
|
theme: WEBVIEW.statusTheme,
|
|
});
|
|
}
|
|
|
|
// Update navigation size
|
|
if (WEBVIEW.navigationEnabled) {
|
|
WEBVIEW.navigation.setBounds({
|
|
x: 0,
|
|
y: window.height - navigation.height,
|
|
width: window.width,
|
|
height: navigation.height,
|
|
});
|
|
WEBVIEW.navigation.webContents.send("data-theme", {
|
|
theme: WEBVIEW.navigationTheme,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Register window events and handler.
|
|
*/
|
|
const windowEvents = async () => {
|
|
console.debug("webview.js: windowEvents()");
|
|
|
|
// Handle window status updates
|
|
WEBVIEW.window.setStatus = async (status) => {
|
|
const apply = (func, ...args) => {
|
|
func.apply(WEBVIEW.window, args);
|
|
return new Promise((r) => setTimeout(r, 50));
|
|
};
|
|
if (WEBVIEW.window.isMinimized()) {
|
|
await apply(WEBVIEW.window.restore);
|
|
}
|
|
switch (status) {
|
|
case "Framed":
|
|
if (WEBVIEW.window.isFullScreen()) {
|
|
await apply(WEBVIEW.window.setFullScreen, false);
|
|
}
|
|
if (WEBVIEW.window.isMaximized()) {
|
|
await apply(WEBVIEW.window.unmaximize);
|
|
}
|
|
break;
|
|
case "Fullscreen":
|
|
if (!WEBVIEW.window.isMaximized()) {
|
|
await apply(WEBVIEW.window.maximize);
|
|
}
|
|
if (!WEBVIEW.window.isFullScreen()) {
|
|
await apply(WEBVIEW.window.setFullScreen, true);
|
|
}
|
|
break;
|
|
case "Maximized":
|
|
if (WEBVIEW.window.isFullScreen()) {
|
|
await apply(WEBVIEW.window.setFullScreen, false);
|
|
}
|
|
if (!WEBVIEW.window.isMaximized()) {
|
|
await apply(WEBVIEW.window.maximize);
|
|
}
|
|
break;
|
|
case "Minimized":
|
|
if (WEBVIEW.window.isFullScreen()) {
|
|
await apply(WEBVIEW.window.setFullScreen, false);
|
|
}
|
|
if (!WEBVIEW.window.isMinimized()) {
|
|
await apply(WEBVIEW.window.minimize);
|
|
}
|
|
break;
|
|
case "Terminated":
|
|
app.quit();
|
|
break;
|
|
}
|
|
};
|
|
WEBVIEW.window.onStatus = () => {
|
|
clearTimeout(WEBVIEW.window.onStatus.timeout);
|
|
WEBVIEW.window.onStatus.timeout = setTimeout(update, 200);
|
|
};
|
|
WEBVIEW.window.on("restore", WEBVIEW.window.onStatus);
|
|
WEBVIEW.window.on("minimize", WEBVIEW.window.onStatus);
|
|
WEBVIEW.window.on("maximize", WEBVIEW.window.onStatus);
|
|
WEBVIEW.window.on("unmaximize", WEBVIEW.window.onStatus);
|
|
WEBVIEW.window.on("enter-full-screen", WEBVIEW.window.onStatus);
|
|
WEBVIEW.window.on("leave-full-screen", WEBVIEW.window.onStatus);
|
|
|
|
// Handle window resize events
|
|
WEBVIEW.window.on("resize", resizeView);
|
|
|
|
// Handle global shortcut events
|
|
globalShortcut.register("Control+Left", () => {
|
|
previousView();
|
|
});
|
|
globalShortcut.register("Control+Right", () => {
|
|
nextView();
|
|
});
|
|
globalShortcut.register("Control+numsub", () => {
|
|
zoomMinus();
|
|
});
|
|
globalShortcut.register("Control+numadd", () => {
|
|
zoomPlus();
|
|
});
|
|
globalShortcut.register("Alt+Left", () => {
|
|
historyBackward();
|
|
});
|
|
globalShortcut.register("Alt+Right", () => {
|
|
historyForward();
|
|
});
|
|
|
|
// Check for window touch events (1s)
|
|
setInterval(() => {
|
|
if (APP.exiting) {
|
|
return;
|
|
}
|
|
const now = new Date();
|
|
const then = WEBVIEW.tracker.pointer.time;
|
|
const delta = (now - then) / 1000;
|
|
|
|
// Auto-hide controls
|
|
if (delta > 60) {
|
|
if (WEBVIEW.tracker.widget.focused) {
|
|
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
|
}
|
|
toggleNavigation("OFF");
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
/**
|
|
* Register widget events and handler.
|
|
*/
|
|
const widgetEvents = async () => {
|
|
if (!WEBVIEW.widgetEnabled) {
|
|
return;
|
|
}
|
|
console.debug("webview.js: widgetEvents()");
|
|
|
|
// Handle widget focus events
|
|
WEBVIEW.widget.webContents.on("focus", () => {
|
|
WEBVIEW.tracker.pointer.time = new Date();
|
|
const window = WEBVIEW.window.getBounds();
|
|
const widget = WEBVIEW.widget.getBounds();
|
|
|
|
// Show widget
|
|
WEBVIEW.widget.setBounds({
|
|
x: window.width - 60,
|
|
y: widget.y,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
});
|
|
WEBVIEW.widget.webContents.send("data-theme", {
|
|
theme: WEBVIEW.widgetTheme,
|
|
});
|
|
WEBVIEW.tracker.widget.focused = true;
|
|
console.debug("webview.js: widgetEvents(widget-focus)");
|
|
});
|
|
|
|
// Handle widget blur events
|
|
WEBVIEW.widget.webContents.on("blur", () => {
|
|
const window = WEBVIEW.window.getBounds();
|
|
const widget = WEBVIEW.widget.getBounds();
|
|
|
|
// Hide widget
|
|
WEBVIEW.widget.setBounds({
|
|
x: window.width - 20,
|
|
y: widget.y,
|
|
width: widget.width,
|
|
height: widget.height,
|
|
});
|
|
WEBVIEW.widget.webContents.send("data-theme", {
|
|
theme: "hidden",
|
|
});
|
|
WEBVIEW.tracker.widget.focused = false;
|
|
console.debug("webview.js: widgetEvents(widget-blur)");
|
|
});
|
|
|
|
// Handle widget button click events
|
|
ipcMain.on("button-click", (e, button) => {
|
|
console.debug(`webview.js: widgetEvents(button-click-${button.id})`);
|
|
WEBVIEW.tracker.pointer.time = new Date();
|
|
switch (button.id) {
|
|
case "fullscreen":
|
|
WEBVIEW.window.setStatus(WEBVIEW.tracker.window.status === "Fullscreen" ? "Framed" : "Fullscreen");
|
|
break;
|
|
case "theme":
|
|
WEBVIEW.theme.toggle();
|
|
break;
|
|
case "keyboard":
|
|
hardware.setKeyboardVisibility(hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON");
|
|
break;
|
|
case "navigation":
|
|
toggleNavigation();
|
|
break;
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Register status events and handler.
|
|
*/
|
|
const statusEvents = async () => {
|
|
if (!WEBVIEW.statusEnabled) {
|
|
return;
|
|
}
|
|
console.debug("webview.js: statusEvents()");
|
|
|
|
// Handle status button click events
|
|
ipcMain.on("button-click", (e, button) => {
|
|
console.debug(`webview.js: statusEvents(button-click-${button.id})`);
|
|
WEBVIEW.tracker.pointer.time = new Date();
|
|
switch (button.id) {
|
|
case "minimize":
|
|
WEBVIEW.window.setStatus("Minimized");
|
|
break;
|
|
case "terminate":
|
|
WEBVIEW.status.webContents.send("button-disabled", { id: "fullscreen", disabled: true });
|
|
WEBVIEW.status.webContents.send("button-disabled", { id: "minimize", disabled: true });
|
|
WEBVIEW.status.webContents.send("button-disabled", { id: "terminate", disabled: true });
|
|
const button = dialog.showMessageBoxSync(WEBVIEW.window, {
|
|
type: "question",
|
|
title: "Confirm",
|
|
message: `\nExit ${APP.title}?`,
|
|
buttons: ["No", "Yes"],
|
|
});
|
|
switch (button) {
|
|
case 1:
|
|
WEBVIEW.window.setStatus("Terminated");
|
|
break;
|
|
default:
|
|
WEBVIEW.status.webContents.send("button-disabled", { id: "fullscreen", disabled: false });
|
|
WEBVIEW.status.webContents.send("button-disabled", { id: "minimize", disabled: false });
|
|
WEBVIEW.status.webContents.send("button-disabled", { id: "terminate", disabled: false });
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Register navigation events and handler.
|
|
*/
|
|
const navigationEvents = async () => {
|
|
if (!WEBVIEW.navigationEnabled) {
|
|
return;
|
|
}
|
|
console.debug("webview.js: navigationEvents()");
|
|
|
|
// Handle input blur events
|
|
let selected = false;
|
|
ipcMain.on("input-blur", (e, input) => {
|
|
console.debug(`webview.js: navigationEvents(input-blur-${input.id})`);
|
|
switch (input.id) {
|
|
case "url":
|
|
if (selected) {
|
|
hardware.setKeyboardVisibility("OFF", () => {
|
|
selected = false;
|
|
WEBVIEW.navigation.webContents.send("input-select", { id: "url", select: selected });
|
|
WEBVIEW.navigation.webContents.send("input-readonly", { id: "url", readonly: !selected });
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Handle input focus events
|
|
ipcMain.on("input-focus", (e, input) => {
|
|
console.debug(`webview.js: navigationEvents(input-focus-${input.id})`);
|
|
WEBVIEW.tracker.pointer.time = new Date();
|
|
switch (input.id) {
|
|
case "url":
|
|
hardware.setKeyboardVisibility("ON", () => {
|
|
setTimeout(() => {
|
|
selected = true;
|
|
WEBVIEW.navigation.webContents.focus();
|
|
WEBVIEW.navigation.webContents.send("input-select", { id: "url", select: selected });
|
|
WEBVIEW.navigation.webContents.send("input-readonly", { id: "url", readonly: !selected });
|
|
}, 400);
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Handle input enter events
|
|
ipcMain.on("input-enter", (e, input) => {
|
|
console.debug(`webview.js: navigationEvents(input-enter-${input.id})`);
|
|
WEBVIEW.tracker.pointer.time = new Date();
|
|
switch (input.id) {
|
|
case "url":
|
|
let url = input.text.trim();
|
|
if (url && !/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(url)) {
|
|
url = "https://" + url;
|
|
}
|
|
if (!url) {
|
|
url = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
|
}
|
|
WEBVIEW.views[WEBVIEW.viewActive].webContents.loadURL(url);
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Handle navigation button click events
|
|
ipcMain.on("button-click", (e, button) => {
|
|
console.debug(`webview.js: navigationEvents(button-click-${button.id})`);
|
|
WEBVIEW.tracker.pointer.time = new Date();
|
|
switch (button.id) {
|
|
case "home":
|
|
homeView();
|
|
break;
|
|
case "refresh":
|
|
reloadView();
|
|
break;
|
|
case "previous":
|
|
previousView();
|
|
break;
|
|
case "next":
|
|
nextView();
|
|
break;
|
|
case "minus":
|
|
zoomMinus();
|
|
break;
|
|
case "plus":
|
|
zoomPlus();
|
|
break;
|
|
case "backward":
|
|
historyBackward();
|
|
break;
|
|
case "forward":
|
|
historyForward();
|
|
break;
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Register view events and handler.
|
|
*/
|
|
const viewEvents = async () => {
|
|
const ready = [];
|
|
const debug = "app_debug" in ARGS;
|
|
|
|
const loaded = (i) => {
|
|
if (WEBVIEW.viewActive !== 0) {
|
|
return true;
|
|
}
|
|
|
|
// Set window status to fullscreen
|
|
if (i === 0 && !debug) {
|
|
WEBVIEW.window.setStatus("Fullscreen");
|
|
}
|
|
|
|
// Hide loader and show first view
|
|
const done = ready.length >= WEBVIEW.views.length;
|
|
if (done) {
|
|
WEBVIEW.viewActive = 1;
|
|
updateView();
|
|
}
|
|
console.debug(`webview.js: viewEvents(${i},loaded-${ready.length}/${WEBVIEW.views.length})`);
|
|
|
|
return done;
|
|
};
|
|
|
|
// Handle events per webview
|
|
WEBVIEW.views.forEach((view, i) => {
|
|
// Enable webview touch emulation
|
|
view.webContents.debugger.attach("1.1");
|
|
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
|
configuration: "mobile",
|
|
enabled: true,
|
|
});
|
|
|
|
// Redirect webview hyperlinks
|
|
view.webContents.setWindowOpenHandler(({ url }) => {
|
|
view.webContents.loadURL(url);
|
|
return { action: "deny" };
|
|
});
|
|
|
|
// Update webview layout
|
|
view.webContents.on("dom-ready", () => {
|
|
console.debug(`webview.js: viewEvents(${i},dom-ready)`);
|
|
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
|
});
|
|
view.webContents.once("dom-ready", () => {
|
|
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
|
ready.push(i);
|
|
});
|
|
|
|
// Webview fully loaded
|
|
view.webContents.on("did-finish-load", () => {
|
|
console.debug(`webview.js: viewEvents(${i},did-finish-load)`);
|
|
if (debug) {
|
|
setTimeout(() => {
|
|
view.webContents.openDevTools();
|
|
}, 2000);
|
|
}
|
|
loaded(i);
|
|
});
|
|
|
|
// Webview not loaded
|
|
view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => {
|
|
if (mainframe) {
|
|
console.debug(`webview.js: viewEvents(${i},did-fail-load)`);
|
|
switch (code) {
|
|
case -3:
|
|
console.warn(`Load Warning: ${url}, ERR_ABORTED (${code})`);
|
|
break;
|
|
default:
|
|
console.error(`Load Error: ${url}, ${text} (${code})`);
|
|
view.webContents.loadURL(errorHtml(code, text, url, WEBVIEW.theme.get()));
|
|
}
|
|
loaded(i);
|
|
}
|
|
});
|
|
|
|
// Webview url changed
|
|
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
|
|
if (mainframe) {
|
|
console.debug(`webview.js: viewEvents(${i},did-navigate-in-page)`);
|
|
updateView();
|
|
}
|
|
});
|
|
view.webContents.on("did-navigate", () => {
|
|
console.debug(`webview.js: viewEvents(${i},did-navigate)`);
|
|
updateView();
|
|
});
|
|
|
|
// Handle webview mouse events
|
|
view.webContents.on("before-mouse-event", (e, mouse) => {
|
|
const now = new Date();
|
|
const then = WEBVIEW.tracker.pointer.time;
|
|
const delta = (now - then) / 1000;
|
|
|
|
// Check mouse event type
|
|
switch (mouse.type) {
|
|
case "mouseMove":
|
|
const posNew = { x: Math.round(mouse.globalX), y: Math.round(mouse.globalY) };
|
|
if (posNew.x < 0 || posNew.y < 0) {
|
|
break;
|
|
}
|
|
console.debug(`webview.js: viewEvents(${i},${mouse.type}-${posNew.x}-${posNew.y})`);
|
|
|
|
// Update tracker pointer time and position
|
|
const posOld = WEBVIEW.tracker.pointer.position;
|
|
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
|
WEBVIEW.tracker.pointer.time = now;
|
|
WEBVIEW.tracker.pointer.position = posNew;
|
|
|
|
// Update last active on pointer position change
|
|
if (delta > 30) {
|
|
console.info("Update Last Active");
|
|
integration.update();
|
|
}
|
|
}
|
|
break;
|
|
case "mouseDown":
|
|
console.debug(`webview.js: viewEvents(${i},${mouse.type}-${mouse.button})`);
|
|
switch (mouse.button) {
|
|
case "left":
|
|
const off = WEBVIEW.tracker.display.off > WEBVIEW.tracker.display.on;
|
|
console.debug(`webview.js: viewEvents(${i},display-${off ? "off" : "on"})`);
|
|
|
|
// Ignore touch event if display was off
|
|
if (off) {
|
|
console.verbose("Display Touch Event: Ignored");
|
|
e.preventDefault();
|
|
|
|
// Turn display on if it was off
|
|
hardware.setDisplayStatus("ON");
|
|
}
|
|
break;
|
|
case "back":
|
|
historyBackward();
|
|
break;
|
|
case "forward":
|
|
historyForward();
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Register app events and handler.
|
|
*/
|
|
const appEvents = async () => {
|
|
console.debug("webview.js: appEvents()");
|
|
|
|
// Handle global events
|
|
EVENTS.on("reloadView", reloadView);
|
|
EVENTS.on("updateView", updateView);
|
|
EVENTS.on("updateDisplay", () => {
|
|
const status = hardware.getDisplayStatus();
|
|
if (status) {
|
|
WEBVIEW.tracker.display[status.toLowerCase()] = new Date();
|
|
}
|
|
});
|
|
EVENTS.on("updateStatus", () => {
|
|
const status = WEBVIEW.tracker.window.status;
|
|
const visibility = hardware.getKeyboardVisibility();
|
|
if (visibility === "ON" && ["Fullscreen", "Minimized"].includes(status)) {
|
|
hardware.setKeyboardVisibility("OFF");
|
|
}
|
|
toggleStatus(["Framed", "Minimized"].includes(status) ? "ON" : "OFF");
|
|
});
|
|
EVENTS.on("updateKeyboard", () => {
|
|
const status = WEBVIEW.tracker.window.status;
|
|
const visibility = hardware.getKeyboardVisibility();
|
|
if (visibility === "ON" && ["Fullscreen"].includes(status)) {
|
|
WEBVIEW.window.setStatus("Maximized");
|
|
} else if (visibility === "OFF" && ["Maximized"].includes(status)) {
|
|
WEBVIEW.window.setStatus("Fullscreen");
|
|
}
|
|
});
|
|
|
|
// Handle multiple instances
|
|
app.on("second-instance", () => {
|
|
if (WEBVIEW.window.isMinimized()) {
|
|
WEBVIEW.window.restore();
|
|
}
|
|
WEBVIEW.window.focus();
|
|
});
|
|
|
|
// Handle crash events
|
|
app.on("render-process-gone", (e, wc, details) => {
|
|
const content = wc.getURL() || "Unknown";
|
|
console.error(`Render Process ${details.reason} (code ${details.exitCode}): ${content}`);
|
|
});
|
|
app.on("child-process-gone", (e, details) => {
|
|
const name = details.name || details.serviceName || "Unknown";
|
|
console.error(`${details.type} Process ${details.reason} (code ${details.exitCode}): ${name}`);
|
|
});
|
|
|
|
// Handle exit events
|
|
app.on("before-quit", () => {
|
|
APP.exiting = true;
|
|
console.warn(`${APP.title} Terminated`);
|
|
});
|
|
process.on("SIGINT", app.quit);
|
|
|
|
// Check latest release (2h)
|
|
setInterval(() => {
|
|
if (APP.exiting) {
|
|
return;
|
|
}
|
|
latestRelease();
|
|
}, 7200 * 1000);
|
|
latestRelease();
|
|
|
|
// Webview initialized
|
|
WEBVIEW.initialized = true;
|
|
};
|
|
|
|
/**
|
|
* Fetches the latest app release infos from github.
|
|
*/
|
|
const latestRelease = async () => {
|
|
try {
|
|
const response = await axios.get(APP.releases.url, { timeout: 20000 });
|
|
const release = response?.data?.find((item) => {
|
|
return !item.draft && (!item.prerelease || "app_early" in ARGS);
|
|
});
|
|
if (release) {
|
|
APP.releases.latest = {
|
|
title: APP.title,
|
|
version: (release.tag_name || " ").replace(/^v/i, ""),
|
|
summary: release.body || " ",
|
|
url: release.html_url || " ",
|
|
};
|
|
EVENTS.emit("updateApp");
|
|
}
|
|
} catch (error) {
|
|
console.warn("Github Error:", error.message);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Checks for network connectivity by requesting a known url.
|
|
*
|
|
* @param {string} url - Url to request.
|
|
* @param {number} interval - Interval between requests in milliseconds.
|
|
* @param {number} timeout - Maximum time to repeat requests in milliseconds.
|
|
* @returns {Promise<boolean>} Resolves true if online, false on timeout.
|
|
*/
|
|
const onlineStatus = (url, interval = 1000, timeout = 60000) => {
|
|
return new Promise((resolve) => {
|
|
const start = Date.now();
|
|
const check = async () => {
|
|
const elapsed = Date.now() - start;
|
|
try {
|
|
if (!url.startsWith("data:")) {
|
|
const agent = new https.Agent({ rejectUnauthorized: !("ignore_certificate_errors" in ARGS) });
|
|
await axios.get(url, { httpsAgent: agent, timeout: 20000 });
|
|
}
|
|
resolve(true);
|
|
} catch (error) {
|
|
if (elapsed >= interval) {
|
|
console.warn(`Checking Connection: ${url}`, error.message);
|
|
}
|
|
if (elapsed >= timeout) {
|
|
if (error.message?.includes("certificate")) {
|
|
console.error("Certificate Error: See https://github.com/leukipp/touchkio/issues/76");
|
|
}
|
|
resolve(false);
|
|
} else {
|
|
setTimeout(check, interval);
|
|
}
|
|
}
|
|
};
|
|
check();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Generates a html template for a spinning loader.
|
|
*
|
|
* @param {number} size - The size of the circle.
|
|
* @param {number} speed - The rotation speed of the circle.
|
|
* @param {string} theme - The theme used for spinner colors.
|
|
* @returns {string} A data string with the generated html.
|
|
*/
|
|
const loaderHtml = (size, speed, theme) => {
|
|
const color = {
|
|
dark: { border: "#2A2A2A", spinner: "#03A9F4", background: "#111111" },
|
|
light: { border: "#DCDCDC", spinner: "#03A9F4", background: "#FAFAFA" },
|
|
}[theme];
|
|
const html = `
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: ${color.background};
|
|
}
|
|
.spinner {
|
|
width: ${size}px;
|
|
height: ${size}px;
|
|
border-radius: 50%;
|
|
border: 4px solid ${color.border};
|
|
border-top-color: ${color.spinner};
|
|
animation: spin ${speed}s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="spinner"></div>
|
|
</body>
|
|
</html>`;
|
|
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
};
|
|
|
|
/**
|
|
* Generates a html template for an error page.
|
|
*
|
|
* @param {number} code - The error code of the response.
|
|
* @param {string} text - The error text of the response.
|
|
* @param {string} url - The url of the requested page.
|
|
* @param {string} theme - The theme used for text colors.
|
|
* @returns {string} A data string with the generated html.
|
|
*/
|
|
const errorHtml = (code, text, url, theme) => {
|
|
const color = {
|
|
dark: { icon: "#FFA500", text: "#E5E5E5", background: "#111111" },
|
|
light: { icon: "#FFA500", text: "#1A1A1A", background: "#FAFAFA" },
|
|
}[theme];
|
|
const html = `
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {
|
|
display: flex;
|
|
align-items: center;
|
|
text-align: center;
|
|
justify-content: center;
|
|
font-family: sans-serif;
|
|
background-color: ${color.background};
|
|
}
|
|
.icon {
|
|
margin: 0;
|
|
font-size: 5rem;
|
|
color: ${color.icon};
|
|
}
|
|
.title {
|
|
margin: 0;
|
|
color: ${color.text};
|
|
}
|
|
.url {
|
|
color: ${color.text};
|
|
}
|
|
.error {
|
|
color: ${color.text};
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div>
|
|
<p class="icon">⚠</p>
|
|
<h1 class="title">Whoopsie!</h1>
|
|
<p class="url"><strong>Loading:</strong> ${url}</p>
|
|
<p class="error"><strong>Error:</strong> ${text} (${code})</p>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
};
|
|
|
|
module.exports = {
|
|
init,
|
|
update,
|
|
};
|