From 1afe3353793cd3afe7bfeada1d57c8f41f80e603 Mon Sep 17 00:00:00 2001 From: leukipp Date: Tue, 8 Jul 2025 14:20:19 +0200 Subject: [PATCH] feat: multi webpage support #25 --- html/navigation.html | 168 +++++++++ {src => html}/widget.html | 41 ++- index.js | 25 +- {src => js}/hardware.js | 0 {src => js}/integration.js | 129 ++++++- js/webview.js | 696 +++++++++++++++++++++++++++++++++++++ src/webview.js | 342 ------------------ 7 files changed, 1017 insertions(+), 384 deletions(-) create mode 100644 html/navigation.html rename {src => html}/widget.html (64%) rename {src => js}/hardware.js (100%) rename {src => js}/integration.js (86%) create mode 100644 js/webview.js delete mode 100644 src/webview.js diff --git a/html/navigation.html b/html/navigation.html new file mode 100644 index 0000000..3b1be65 --- /dev/null +++ b/html/navigation.html @@ -0,0 +1,168 @@ + + + + + + + +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
    + + + diff --git a/src/widget.html b/html/widget.html similarity index 64% rename from src/widget.html rename to html/widget.html index bd52174..646f32a 100644 --- a/src/widget.html +++ b/html/widget.html @@ -61,11 +61,11 @@
  • - @@ -91,11 +91,11 @@
  • - @@ -104,22 +104,25 @@ diff --git a/index.js b/index.js index 8e930a7..f1edb4c 100644 --- a/index.js +++ b/index.js @@ -2,9 +2,9 @@ const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); const readline = require("readline/promises"); -const integration = require("./src/integration"); -const hardware = require("./src/hardware"); -const webview = require("./src/webview"); +const integration = require("./js/integration"); +const hardware = require("./js/hardware"); +const webview = require("./js/webview"); const { app } = require("electron"); global.ARGS = global.ARGS || {}; @@ -44,6 +44,12 @@ app.whenReady().then(async () => { } else if (!argsProvided && argsFileExists) { args = readArgs(argsFilePath); } + + // Split url arguments + args.web_url = args.web_url || []; + if (!Array.isArray(args.web_url)) { + args.web_url = args.web_url.split(",").map((url) => url.trim()); + } global.ARGS = args; // Show used arguments @@ -68,8 +74,11 @@ const parseArgs = (proc) => { let args = {}; proc.argv.slice(1).forEach((arg) => { if (arg !== ".") { - const [key, value] = /^(.*?)=(.*)$/.exec(arg).slice(1); - args[key.replace("--", "").replace("-", "_")] = value; + const match = /^(.*?)=(.*)$/.exec(arg); + if (match) { + const [key, value] = match.slice(1); + args[key.replace("--", "").replace("-", "_")] = value; + } } }); return args; @@ -164,7 +173,11 @@ const promptArgs = async (proc) => { const prompt = `${question} (${fallback}): `; const answer = await read.question(prompt); const value = answer.trim() || fallback; - args[key] = value; + if (key === "web_url") { + args[key] = value.split(",").map((v) => v.trim()); + } else { + args[key] = value; + } } } read.close(); diff --git a/src/hardware.js b/js/hardware.js similarity index 100% rename from src/hardware.js rename to js/hardware.js diff --git a/src/integration.js b/js/integration.js similarity index 86% rename from src/integration.js rename to js/integration.js index 30043e0..2ec283b 100644 --- a/src/integration.js +++ b/js/integration.js @@ -10,7 +10,7 @@ global.INTEGRATION = global.INTEGRATION || { /** * Initializes the integration with the provided arguments. * - * @returns {boolean} Returns true if the initialization was successful. + * @returns {bool} Returns true if the initialization was successful. */ const init = async () => { if (!ARGS.mqtt_url) { @@ -47,7 +47,7 @@ const init = async () => { }; // Connection settings - const options = user === null || password == null ? null : { username: user, password: password }; + const options = user === null || password === null ? null : { username: user, password: password }; const masked = password === null ? "null" : "*".repeat(password.length); console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`); INTEGRATION.client = mqtt.connect(url.toString(), options); @@ -64,6 +64,8 @@ const init = async () => { initKiosk(INTEGRATION.client); initDisplay(INTEGRATION.client); initKeyboard(INTEGRATION.client); + initPageNumber(INTEGRATION.client); + initPageUrl(INTEGRATION.client); // Init client sensors initModel(INTEGRATION.client); @@ -124,6 +126,8 @@ const update = async () => { // Update client sensors updateKiosk(INTEGRATION.client); + updatePageNumber(INTEGRATION.client); + updatePageUrl(INTEGRATION.client); updateUpTime(INTEGRATION.client); updateLastActive(INTEGRATION.client); updateMemoryUsage(INTEGRATION.client); @@ -203,7 +207,7 @@ const initRefresh = (client) => { if (topic === `${root}/execute`) { console.log("Refreshing webview..."); hardware.setDisplayStatus("ON"); - WEBVIEW.view.webContents.reloadIgnoringCache(); + WEBVIEW.views[WEBVIEW.viewActive].webContents.reloadIgnoringCache(); } }) .subscribe(`${root}/execute`); @@ -217,7 +221,7 @@ const initRefresh = (client) => { const initKiosk = (client) => { const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`; const config = { - name: `Kiosk`, + name: "Kiosk", unique_id: `${INTEGRATION.node}_kiosk`, command_topic: `${root}/set`, state_topic: `${root}/status`, @@ -287,7 +291,7 @@ const initDisplay = (client) => { } const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`; const config = { - name: `Display`, + name: "Display", unique_id: `${INTEGRATION.node}_display`, command_topic: `${root}/set`, state_topic: `${root}/status`, @@ -348,7 +352,7 @@ const initKeyboard = (client) => { } const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`; const config = { - name: `Keyboard`, + name: "Keyboard", unique_id: `${INTEGRATION.node}_keyboard`, command_topic: `${root}/set`, state_topic: `${root}/status`, @@ -393,6 +397,97 @@ const updateKeyboard = (client) => { } }; +/** + * Initializes the page number and handles the execute logic. + * + * @param {Object} client - Instance of the mqtt client. + */ +const initPageNumber = (client) => { + const root = `${INTEGRATION.discovery}/number/${INTEGRATION.node}/page_number`; + const config = { + name: "View", + unique_id: `${INTEGRATION.node}_page_number`, + command_topic: `${root}/set`, + state_topic: `${root}/status`, + value_template: "{{ value }}", + mode: "box", + min: 1, + max: WEBVIEW.viewUrls.length - 1, + unit_of_measurement: "Page", + icon: "mdi:page-next", + device: INTEGRATION.device, + }; + client + .publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true }) + .on("message", (topic, message) => { + if (topic === `${root}/set`) { + const number = parseInt(message, 10); + console.log("Set Page Number:", number); + WEBVIEW.viewActive = number; + } + }) + .subscribe(`${root}/set`); + updatePageNumber(client); +}; + +/** + * Updates the page number via the mqtt connection. + * + * @param {Object} client - Instance of the mqtt client. + */ +const updatePageNumber = (client) => { + const pageNumber = WEBVIEW.viewActive; + const root = `${INTEGRATION.discovery}/number/${INTEGRATION.node}/page_number`; + if (pageNumber !== null) { + client.publish(`${root}/status`, `${pageNumber}`, { qos: 1, retain: true }); + } +}; + +/** + * Initializes the page url and handles the execute logic. + * + * @param {Object} client - Instance of the mqtt client. + */ +const initPageUrl = (client) => { + const root = `${INTEGRATION.discovery}/text/${INTEGRATION.node}/page_url`; + const config = { + name: "Url", + unique_id: `${INTEGRATION.node}_page_url`, + command_topic: `${root}/set`, + state_topic: `${root}/status`, + value_template: "{{ value }}", + pattern: "https?://.*", + icon: "mdi:web", + device: INTEGRATION.device, + }; + client + .publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true }) + .on("message", (topic, message) => { + if (topic === `${root}/set`) { + const url = message.toString(); + console.log("Set Page Url:", url); + WEBVIEW.views[WEBVIEW.viewActive].webContents.loadURL(url); + } + }) + .subscribe(`${root}/set`); + updatePageUrl(client); +}; + +/** + * Updates the page url via the mqtt connection. + * + * @param {Object} client - Instance of the mqtt client. + */ +const updatePageUrl = (client) => { + const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; + const currentUrl = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL(); + const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl; + const root = `${INTEGRATION.discovery}/text/${INTEGRATION.node}/page_url`; + if (pageUrl !== null) { + client.publish(`${root}/status`, `${pageUrl}`, { qos: 1, retain: true }); + } +}; + /** * Initializes the model sensor. * @@ -401,7 +496,7 @@ const updateKeyboard = (client) => { const initModel = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`; const config = { - name: `Model`, + name: "Model", unique_id: `${INTEGRATION.node}_model`, state_topic: `${root}/status`, value_template: "{{ value }}", @@ -433,7 +528,7 @@ const updateModel = (client) => { const initSerialNumber = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`; const config = { - name: `Serial Number`, + name: "Serial Number", unique_id: `${INTEGRATION.node}_serial_number`, state_topic: `${root}/status`, value_template: "{{ value }}", @@ -465,7 +560,7 @@ const updateSerialNumber = (client) => { const initHostName = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`; const config = { - name: `Host Name`, + name: "Host Name", unique_id: `${INTEGRATION.node}_host_name`, state_topic: `${root}/status`, value_template: "{{ value }}", @@ -497,7 +592,7 @@ const updateHostName = (client) => { const initUpTime = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`; const config = { - name: `Up Time`, + name: "Up Time", unique_id: `${INTEGRATION.node}_up_time`, state_topic: `${root}/status`, value_template: "{{ (value | float) | round(0) }}", @@ -530,7 +625,7 @@ const updateUpTime = (client) => { const initMemorySize = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`; const config = { - name: `Memory Size`, + name: "Memory Size", unique_id: `${INTEGRATION.node}_memory_size`, state_topic: `${root}/status`, value_template: "{{ (value | float) | round(2) }}", @@ -563,7 +658,7 @@ const updateMemorySize = (client) => { const initMemoryUsage = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`; const config = { - name: `Memory Usage`, + name: "Memory Usage", unique_id: `${INTEGRATION.node}_memory_usage`, state_topic: `${root}/status`, value_template: "{{ (value | float) | round(0) }}", @@ -596,7 +691,7 @@ const updateMemoryUsage = (client) => { const initProcessorUsage = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`; const config = { - name: `Processor Usage`, + name: "Processor Usage", unique_id: `${INTEGRATION.node}_processor_usage`, state_topic: `${root}/status`, value_template: "{{ (value | float) | round(0) }}", @@ -629,7 +724,7 @@ const updateProcessorUsage = (client) => { const initProcessorTemperature = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`; const config = { - name: `Processor Temperature`, + name: "Processor Temperature", unique_id: `${INTEGRATION.node}_processor_temperature`, state_topic: `${root}/status`, value_template: "{{ (value | float) | round(0) }}", @@ -662,7 +757,7 @@ const updateProcessorTemperature = (client) => { const initPackageUpgrades = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`; const config = { - name: `Package Upgrades`, + name: "Package Upgrades", unique_id: `${INTEGRATION.node}_package_upgrades`, state_topic: `${root}/status`, json_attributes_topic: `${root}/attributes`, @@ -700,7 +795,7 @@ const updatePackageUpgrades = (client) => { const initHeartbeat = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`; const config = { - name: `Heartbeat`, + name: "Heartbeat", unique_id: `${INTEGRATION.node}_heartbeat`, state_topic: `${root}/status`, value_template: "{{ value }}", @@ -732,7 +827,7 @@ const updateHeartbeat = (client) => { const initLastActive = (client) => { const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`; const config = { - name: `Last Active`, + name: "Last Active", unique_id: `${INTEGRATION.node}_last_active`, state_topic: `${root}/status`, value_template: "{{ (value | float) | round(0) }}", diff --git a/js/webview.js b/js/webview.js new file mode 100644 index 0000000..8911b1a --- /dev/null +++ b/js/webview.js @@ -0,0 +1,696 @@ +const path = require("path"); +const hardware = require("./hardware"); +const integration = require("./integration"); +const { app, screen, nativeTheme, ipcMain, BaseWindow, WebContentsView } = require("electron"); + +global.WEBVIEW = global.WEBVIEW || { + initialized: false, + status: null, + locked: false, + pointer: { + position: {}, + time: new Date(), + }, +}; + +/** + * 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(); + } + const fullscreen = ARGS.app_debug !== "true"; + 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.viewUrls = urls; + WEBVIEW.viewZoom = zoom; + WEBVIEW.viewTheme = theme; + WEBVIEW.widgetTheme = theme; + WEBVIEW.widgetEnabled = widget; + WEBVIEW.navigationTheme = theme; + WEBVIEW.navigationEnabled = widget; + nativeTheme.themeSource = WEBVIEW.viewTheme; + Object.defineProperty(WEBVIEW, "viewActive", { + get() { + return this._viewActive || 0; + }, + set(v) { + this._viewActive = Math.max(0, Math.min(v, WEBVIEW.viewUrls.length - 1)); + updateView(); + }, + }); + + // Init global root window + WEBVIEW.window = new BaseWindow({ + title: "TouchKio", + icon: path.join(__dirname, "..", "img", "icon.png"), + }); + WEBVIEW.window.setMenuBarVisibility(false); + WEBVIEW.window.setFullScreen(fullscreen); + if (!fullscreen) { + WEBVIEW.window.maximize(); + } + + // 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); + view.webContents.loadURL(url); + WEBVIEW.views.push(view); + }); + + // 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.getAppPath(), "html", "widget.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.getAppPath(), "html", "navigation.html")); + + // Register events + windowEvents(); + widgetEvents(); + navigationEvents(); + domEvents(); + appEvents(); + + return true; +}; + +/** + * Updates the shared webview properties. + */ +const update = async () => { + if (!WEBVIEW.initialized) { + return; + } + + // Update windows status + if (WEBVIEW.window.isFullScreen()) { + WEBVIEW.status = "Fullscreen"; + } else if (WEBVIEW.window.isMinimized()) { + WEBVIEW.status = "Minimized"; + } else if (WEBVIEW.window.isMaximized()) { + WEBVIEW.status = "Maximized"; + } else { + WEBVIEW.status = "Framed"; + } + + // Update widget status + updateWidget(); + + // Update navigation status + updateNavigation(); + + // Update integration sensor + console.log("Update Kiosk Status:", WEBVIEW.status); + integration.update(); +}; + +/** + * Updates the active view. + */ +const updateView = () => { + const title = `TouchKio - ${new URL(WEBVIEW.viewUrls[WEBVIEW.viewActive]).host}`; + console.log(`Update View: ${title} (${WEBVIEW.viewActive})`); + 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 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 = () => { + const view = WEBVIEW.views[WEBVIEW.viewActive]; + const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; + const currentUrl = view.webContents.getURL(); + + // Disable pager button + WEBVIEW.navigation.webContents.send("button-disabled", { + id: "pager", + disabled: WEBVIEW.views.length < 3, + }); + + // Update url text + WEBVIEW.navigation.webContents.send("input-text", { + id: "url", + text: currentUrl.startsWith("data:") ? "" : currentUrl, + placeholder: defaultUrl, + }); + + // 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. + */ +const toggleNavigation = () => { + const window = WEBVIEW.window.getBounds(); + const navigation = WEBVIEW.navigation.getBounds(); + const height = navigation.height > 0 ? 0 : 60; + + // Show or hide navigation based on height + WEBVIEW.navigation.setBounds({ + x: 0, + y: window.height - height, + width: window.width, + height: height, + }); + + // Resize webview + resizeView(); +}; + +/** + * Navigates backward in the history of the active webview. + */ +const historyBackward = () => { + 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 = () => { + const view = WEBVIEW.views[WEBVIEW.viewActive]; + if (view.webContents.navigationHistory.canGoForward()) { + view.webContents.navigationHistory.goForward(); + } +}; + +/** + * Activates the next webview or cycles back to the first. + */ +const nextView = () => { + WEBVIEW.viewActive = (WEBVIEW.viewActive + 1) % WEBVIEW.views.length || 1; +}; + +/** + * Reloads the current or default url on the active webview. + */ +const reloadView = () => { + const view = WEBVIEW.views[WEBVIEW.viewActive]; + const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; + const currentUrl = view.webContents.getURL(); + + // Reload the default url or refresh the page + if (currentUrl != defaultUrl) { + view.webContents.loadURL(defaultUrl); + } else { + view.webContents.reloadIgnoringCache(); + } +}; + +/** + * Resizes and positions all webviews. + */ +const resizeView = () => { + const window = WEBVIEW.window.getBounds(); + const navigation = WEBVIEW.navigation.getBounds(); + const widget = { width: 60, height: 200 }; + + // Update view size + WEBVIEW.views.forEach((view) => { + view.setBounds({ + x: 0, + y: 0, + width: window.width, + height: window.height - navigation.height, + }); + view.webContents.setZoomFactor(WEBVIEW.viewZoom); + }); + + // Update widget size + if (WEBVIEW.widgetEnabled) { + WEBVIEW.widget.setBounds({ + x: window.width - 15, + y: parseInt(window.height / 2 - widget.height / 2, 10), + width: widget.width, + height: widget.height, + }); + WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" }); + } + + // 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 = () => { + // Handle window resize events + WEBVIEW.window.on("ready-to-show", resizeView); + WEBVIEW.window.on("resize", resizeView); + resizeView(); + + // Handle window focus events + WEBVIEW.window.on("focus", () => { + if (WEBVIEW.locked) { + hardware.setDisplayStatus("ON"); + WEBVIEW.window.blur(); + } + WEBVIEW.locked = false; + }); + HARDWARE.display.notifiers.push(() => { + WEBVIEW.locked = hardware.getDisplayStatus() === "OFF"; + if (WEBVIEW.locked) { + WEBVIEW.window.blur(); + } + }); + + // Handle window status updates + WEBVIEW.window.on("minimize", update); + WEBVIEW.window.on("restore", update); + WEBVIEW.window.on("maximize", update); + WEBVIEW.window.on("unmaximize", update); + WEBVIEW.window.on("enter-full-screen", update); + WEBVIEW.window.on("leave-full-screen", update); + + // Handle window touch events for activity tracking + setInterval(() => { + const posOld = WEBVIEW.pointer.position; + const posNew = screen.getCursorScreenPoint(); + if (posOld.x !== posNew.x || posOld.y !== posNew.y) { + const now = new Date(); + const then = WEBVIEW.pointer.time; + + // Update integration sensor + WEBVIEW.pointer.time = now; + if (Math.abs(now - then) / 1000 > 30) { + console.log("Update Last Active"); + integration.update(); + } + } + WEBVIEW.pointer.position = posNew; + }, 1000); +}; + +/** + * Register widget events and handler. + */ +const widgetEvents = () => { + if (!WEBVIEW.widgetEnabled) { + return; + } + + // Handle widget focus events + WEBVIEW.widget.webContents.on("focus", () => { + 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 }); + }); + + // 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 - 15, + y: widget.y, + width: widget.width, + height: widget.height, + }); + WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" }); + }); + + // Handle widget button click events + ipcMain.on("button-click", (e, button) => { + switch (button.id) { + case "keyboard": + const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON"; + hardware.setKeyboardVisibility(toggle); + switch (toggle) { + case "OFF": + WEBVIEW.window.restore(); + WEBVIEW.window.unmaximize(); + WEBVIEW.window.setFullScreen(true); + break; + case "ON": + WEBVIEW.window.restore(); + WEBVIEW.window.setFullScreen(false); + WEBVIEW.window.maximize(); + break; + } + break; + case "fullscreen": + if (WEBVIEW.window.isFullScreen()) { + WEBVIEW.window.restore(); + WEBVIEW.window.unmaximize(); + WEBVIEW.window.setFullScreen(false); + } else { + WEBVIEW.window.restore(); + WEBVIEW.window.unmaximize(); + WEBVIEW.window.setFullScreen(true); + } + hardware.setKeyboardVisibility("OFF"); + break; + case "minimize": + WEBVIEW.window.restore(); + WEBVIEW.window.setFullScreen(false); + WEBVIEW.window.minimize(); + hardware.setKeyboardVisibility("OFF"); + break; + case "navigation": + toggleNavigation(); + break; + } + }); +}; + +/** + * Register navigation events and handler. + */ +const navigationEvents = () => { + if (!WEBVIEW.navigationEnabled) { + return; + } + + // Handle navigation blur events + let ignoreBlur = false; + WEBVIEW.navigation.webContents.on("blur", () => { + const visibility = hardware.getKeyboardVisibility(); + if (visibility === "ON" && !ignoreBlur) { + WEBVIEW.window.restore(); + WEBVIEW.window.unmaximize(); + WEBVIEW.window.setFullScreen(true); + hardware.setKeyboardVisibility("OFF"); + WEBVIEW.navigation.webContents.send("input-blur", { id: "url" }); + } + }); + + // Handle input focus events + ipcMain.on("input-focus", (e, input) => { + const visibility = hardware.getKeyboardVisibility(); + switch (input.id) { + case "url": + if (visibility === "OFF") { + ignoreBlur = true; + WEBVIEW.window.restore(); + WEBVIEW.window.setFullScreen(false); + WEBVIEW.window.maximize(); + hardware.setKeyboardVisibility("ON", () => { + setTimeout(() => { + WEBVIEW.navigation.webContents.focus(); + WEBVIEW.navigation.webContents.send("input-select", { id: "url" }); + ignoreBlur = false; + }, 500); + }); + } + break; + } + }); + + // Handle input enter events + ipcMain.on("input-enter", (e, input) => { + 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) => { + switch (button.id) { + case "pager": + nextView(); + hardware.setKeyboardVisibility("OFF"); + break; + case "home": + reloadView(); + hardware.setKeyboardVisibility("OFF"); + break; + case "backward": + historyBackward(); + hardware.setKeyboardVisibility("OFF"); + break; + case "forward": + historyForward(); + hardware.setKeyboardVisibility("OFF"); + break; + } + }); +}; + +/** + * Register dom events and handler. + */ +const domEvents = () => { + WEBVIEW.views.forEach((view) => { + // 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" }; + }); + + // Remove webview scrollbar + view.webContents.on("dom-ready", () => { + view.webContents.insertCSS("::-webkit-scrollbar { display: none; }"); + }); + + // Webview fully loaded + view.webContents.on("did-finish-load", () => { + WEBVIEW.initialized = true; + if (WEBVIEW.viewActive === 0) { + nextView(); + } + if (ARGS.app_debug === "true") { + setTimeout(() => { + view.webContents.openDevTools(); + }, 2000); + } + }); + + // Webview not loaded + view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => { + if (mainframe) { + if (WEBVIEW.viewActive === 0) { + nextView(); + } + view.webContents.loadURL(errorHtml(code, text, url)); + } + }); + + // Webview url changed + view.webContents.on("did-navigate-in-page", (e, url, mainframe) => { + if (mainframe) { + update(); + } + }); + view.webContents.on("did-navigate", () => { + update(); + }); + }); +}; + +/** + * Register app events and handler. + */ +const appEvents = () => { + // Handle signal and exit events + process.on("SIGINT", app.quit); + app.on("before-quit", () => { + WEBVIEW.status = "Terminated"; + integration.update(); + }); + + // Handle multiple instances + app.on("second-instance", () => { + if (WEBVIEW.window.isMinimized()) { + WEBVIEW.window.restore(); + } + WEBVIEW.window.focus(); + }); +}; + +/** + * 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 = ` + + + + + +
    + + `; + 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. + * @returns {string} A data string with the generated html. + */ +const errorHtml = (code, text, url) => { + const html = ` + + + + + +
    +

    ⚠

    +

    Whoopsie!

    +

    Loading: ${url}

    +

    Error: ${text} (${code})

    +
    + + `; + return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; +}; + +module.exports = { + init, + update, +}; diff --git a/src/webview.js b/src/webview.js deleted file mode 100644 index ce652c5..0000000 --- a/src/webview.js +++ /dev/null @@ -1,342 +0,0 @@ -const path = require("path"); -const hardware = require("./hardware"); -const integration = require("./integration"); -const { app, screen, nativeTheme, ipcMain, BaseWindow, WebContentsView } = require("electron"); - -global.WEBVIEW = global.WEBVIEW || { - initialized: false, - status: null, - locked: false, - pointer: { - position: {}, - time: new Date(), - }, -}; - -/** - * Initializes the webview with the provided arguments. - * - * @returns {boolean} Returns true if the initialization was successful. - */ -const init = async () => { - if (!ARGS.web_url) { - console.error("Please provide the '--web-url' parameter"); - return app.quit(); - } - if (!/^https?:\/\//.test(ARGS.web_url)) { - console.error("Please provide the '--web-url' parameter with http(s)"); - return app.quit(); - } - const url = new URL(ARGS.web_url); - const theme = ["light", "dark"].includes(ARGS.web_theme) ? ARGS.web_theme : "dark"; - const zoom = !isNaN(parseFloat(ARGS.web_zoom)) ? parseFloat(ARGS.web_zoom) : 1.25; - const widget = ARGS.web_widget ? ARGS.web_widget === "true" : true; - const fullscreen = ARGS.app_debug !== "true"; - - // Init global layout - WEBVIEW.viewZoom = zoom; - WEBVIEW.viewTheme = theme; - WEBVIEW.widgetTheme = theme; - WEBVIEW.widgetEnabled = widget; - nativeTheme.themeSource = WEBVIEW.viewTheme; - - // Init global root window - WEBVIEW.window = new BaseWindow({ - title: `TouchKio - ${url.host}`, - icon: path.join(__dirname, "..", "img", "icon.png"), - }); - WEBVIEW.window.setMenuBarVisibility(false); - WEBVIEW.window.setFullScreen(fullscreen); - if (!fullscreen) { - WEBVIEW.window.maximize(); - } - - // Init global webview - WEBVIEW.view = new WebContentsView({ - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - }, - }); - WEBVIEW.window.contentView.addChildView(WEBVIEW.view); - WEBVIEW.view.webContents.loadURL(url.toString()); - WEBVIEW.view.setBackgroundColor("#FFFFFFFF"); - - // Init global widget - WEBVIEW.widget = new WebContentsView({ - transparent: true, - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - }, - }); - WEBVIEW.window.contentView.addChildView(WEBVIEW.widget); - WEBVIEW.widget.webContents.loadFile(path.join(__dirname, "widget.html")); - WEBVIEW.widget.setBackgroundColor("#00000000"); - - // Register events - windowEvents(); - widgetEvents(); - domEvents(); - appEvents(); - - return true; -}; - -/** - * Updates the shared webview properties. - */ -const update = async () => { - if (!WEBVIEW.initialized) { - return; - } - - // Update window status - if (WEBVIEW.window.isFullScreen()) { - WEBVIEW.status = "Fullscreen"; - } else if (WEBVIEW.window.isMinimized()) { - WEBVIEW.status = "Minimized"; - } else if (WEBVIEW.window.isMaximized()) { - WEBVIEW.status = "Maximized"; - } else { - WEBVIEW.status = "Framed"; - } - - // Update widget status - if (!HARDWARE.support.keyboardVisibility) { - WEBVIEW.widget.webContents.send("button-hide", { id: "keyboard" }); - } - - // Update integration sensor - console.log("Update Kiosk Status:", WEBVIEW.status); - integration.update(); -}; - -/** - * Register window events and handler. - */ -const windowEvents = () => { - // Handle window resize events - const resize = () => { - const window = WEBVIEW.window.getBounds(); - const widget = { width: 60, height: 200 }; - - // Update view size - WEBVIEW.view.setBounds({ - x: 0, - y: 0, - width: window.width, - height: window.height, - }); - WEBVIEW.view.webContents.setZoomFactor(WEBVIEW.viewZoom); - WEBVIEW.view.webContents.focus(); - - // Update widget size - if (WEBVIEW.widgetEnabled) { - WEBVIEW.widget.setBounds({ - x: window.width - 15, - y: parseInt(window.height / 2 - widget.height / 2), - width: widget.width, - height: widget.height, - }); - WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" }); - } - }; - WEBVIEW.window.on("ready-to-show", resize); - WEBVIEW.window.on("resize", resize); - resize(); - - // Handle window focus events - WEBVIEW.window.on("focus", () => { - if (WEBVIEW.locked) { - hardware.setDisplayStatus("ON"); - WEBVIEW.window.blur(); - } - WEBVIEW.locked = false; - }); - HARDWARE.display.notifiers.push(() => { - WEBVIEW.locked = hardware.getDisplayStatus() === "OFF"; - if (WEBVIEW.locked) { - WEBVIEW.window.blur(); - } - }); - - // Handle window status updates - WEBVIEW.window.on("minimize", update); - WEBVIEW.window.on("restore", update); - WEBVIEW.window.on("maximize", update); - WEBVIEW.window.on("unmaximize", update); - WEBVIEW.window.on("enter-full-screen", update); - WEBVIEW.window.on("leave-full-screen", update); - - // Handle window touch events for activity tracking - setInterval(() => { - const posOld = WEBVIEW.pointer.position; - const posNew = screen.getCursorScreenPoint(); - if (posOld.x !== posNew.x || posOld.y !== posNew.y) { - const now = new Date(); - const then = WEBVIEW.pointer.time; - - // Update integration sensor - WEBVIEW.pointer.time = now; - if (Math.abs(now - then) / 1000 > 30) { - console.log("Update Last Active"); - integration.update(); - } - } - WEBVIEW.pointer.position = posNew; - }, 1 * 1000); -}; - -/** - * Register widget events and handler. - */ -const widgetEvents = () => { - if (!WEBVIEW.widgetEnabled) { - return; - } - - // Handle widget focus events - WEBVIEW.widget.webContents.on("focus", () => { - 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 }); - }); - - // 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 - 15, - y: widget.y, - width: widget.width, - height: widget.height, - }); - WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" }); - }); - - // Handle widget button click events - ipcMain.on("button-click", (e, button) => { - switch (button.id) { - case "refresh": - WEBVIEW.view.webContents.reloadIgnoringCache(); - break; - case "fullscreen": - if (WEBVIEW.window.isFullScreen()) { - WEBVIEW.window.restore(); - WEBVIEW.window.unmaximize(); - WEBVIEW.window.setFullScreen(false); - } else { - WEBVIEW.window.restore(); - WEBVIEW.window.unmaximize(); - WEBVIEW.window.setFullScreen(true); - } - break; - case "minimize": - WEBVIEW.window.restore(); - WEBVIEW.window.setFullScreen(false); - WEBVIEW.window.minimize(); - break; - case "keyboard": - const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON"; - hardware.setKeyboardVisibility(toggle); - switch (toggle) { - case "OFF": - WEBVIEW.window.restore(); - WEBVIEW.window.unmaximize(); - WEBVIEW.window.setFullScreen(true); - break; - case "ON": - WEBVIEW.window.restore(); - WEBVIEW.window.setFullScreen(false); - WEBVIEW.window.maximize(); - break; - } - break; - } - }); -}; - -/** - * Register dom events and handler. - */ -const domEvents = () => { - // Enable webview touch emulation - WEBVIEW.view.webContents.debugger.attach("1.1"); - WEBVIEW.view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", { - configuration: "mobile", - enabled: true, - }); - - // Disable webview hyperlinks - WEBVIEW.view.webContents.setWindowOpenHandler(() => { - return { action: "deny" }; - }); - - // Remove webview scrollbar - WEBVIEW.view.webContents.on("dom-ready", () => { - WEBVIEW.view.webContents.insertCSS("::-webkit-scrollbar { display: none; }"); - }); - - // Webview fully loaded - WEBVIEW.view.webContents.on("did-finish-load", () => { - if (ARGS.app_debug === "true") { - WEBVIEW.view.webContents.openDevTools(); - } - WEBVIEW.initialized = true; - update(); - }); - - // Webview not loaded - WEBVIEW.view.webContents.on("did-fail-load", (e, code, description, url, mainframe) => { - if (mainframe) { - const html = ` - - -
    -

    Whoopsie!

    -

    Loading: ${url}

    -

    Error: ${description} (${code})

    -
    - - `; - WEBVIEW.view.webContents.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`); - } - }); -}; - -/** - * Register app events and handler. - */ -const appEvents = () => { - // Handle signal and exit events - process.on("SIGINT", app.quit); - app.on("before-quit", () => { - WEBVIEW.status = "Terminated"; - integration.update(); - }); - - // Handle multiple instances - app.on("second-instance", () => { - if (WEBVIEW.window.isMinimized()) { - WEBVIEW.window.restore(); - } - WEBVIEW.window.focus(); - }); -}; - -module.exports = { - init, - update, -};