feat: multi webpage support #25

This commit is contained in:
leukipp
2025-07-08 14:20:19 +02:00
parent 568be07043
commit 1afe335379
7 changed files with 1017 additions and 384 deletions
+168
View File
@@ -0,0 +1,168 @@
<!doctype html>
<html>
<head>
<style>
[data-theme="dark"] {
--primary-color: rgba(225, 225, 225, 1);
--border-color: rgba(225, 225, 225, 0.15);
--background-color: rgba(28, 28, 28, 1);
}
[data-theme="light"] {
--primary-color: rgba(33, 33, 33, 1);
--border-color: rgba(33, 33, 33, 0.15);
--background-color: rgba(255, 255, 255, 1);
}
::-webkit-scrollbar {
display: none;
}
* {
margin: 0;
padding: 0;
cursor: pointer;
}
body {
display: flex;
align-items: center;
width: 100vw;
height: 100vh;
}
menu {
display: flex;
flex-direction: row;
align-items: center;
width: 100vw;
height: 100vh;
padding: 0 30px;
column-gap: 20px;
list-style-type: none;
border-top: 1px solid var(--border-color);
background: var(--background-color);
}
li {
display: flex;
align-items: center;
flex: 0 0 auto;
}
li.dynamic {
flex: 1 1 0%;
}
button {
width: 32px;
height: 32px;
border: none;
background-color: transparent;
}
button.disabled {
opacity: 0.5;
pointer-events: none;
}
input {
width: 100%;
min-width: 0;
margin: 0 10px;
padding: 6px 10px;
border-radius: 4px;
font-size: 18px;
outline: none;
border: none;
box-sizing: border-box;
color: var(--primary-color);
background-color: var(--border-color);
}
svg {
pointer-events: none;
fill: var(--primary-color);
}
</style>
</head>
<body>
<menu>
<li>
<button id="home">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>home-circle</title>
<path
d="M19.07,4.93C17.22,3 14.66,1.96 12,2C9.34,1.96 6.79,3 4.94,4.93C3,6.78 1.96,9.34 2,12C1.96,14.66 3,17.21 4.93,19.06C6.78,21 9.34,22.04 12,22C14.66,22.04 17.21,21 19.06,19.07C21,17.22 22.04,14.66 22,12C22.04,9.34 21,6.78 19.07,4.93M17,12V18H13.5V13H10.5V18H7V12H5L12,5L19.5,12H17Z"
/>
</svg>
</button>
</li>
<li>
<button id="pager">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>page-next</title>
<path
d="M20,3H5A2,2 0 0,0 3,5V11H7V9L11,12L7,15V13H3V19A2,2 0 0,0 5,21H20A2,2 0 0,0 22,19V5A2,2 0 0,0 20,3M17,17H13V15H17V17M20,13H13V11H20V13M20,9H13V7H20V9M3,13H0V11H3V13Z"
/>
</svg>
</button>
</li>
<li class="dynamic">
<input id="url" type="text" spellcheck="false" />
</li>
<li>
<button id="backward">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>arrow-left-circle</title>
<path
d="M2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12M18,11H10L13.5,7.5L12.08,6.08L6.16,12L12.08,17.92L13.5,16.5L10,13H18V11Z"
/>
</svg>
</button>
</li>
<li>
<button id="forward">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>arrow-right-circle</title>
<path
d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M6,13H14L10.5,16.5L11.92,17.92L17.84,12L11.92,6.08L10.5,7.5L14,11H6V13Z"
/>
</svg>
</button>
</li>
</menu>
<script>
const { ipcRenderer } = require("electron");
const registerSendEvents = () => {
document.querySelectorAll("button").forEach((button) => {
button.addEventListener("click", (e) => {
ipcRenderer.send("button-click", { id: e.target.id });
});
});
document.querySelectorAll("input").forEach((input) => {
input.addEventListener("focus", (e) => {
ipcRenderer.send("input-focus", { id: e.target.id });
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
ipcRenderer.send("input-enter", { id: e.target.id, text: e.target.value });
}
});
});
};
const registerReceiveEvents = () => {
ipcRenderer.on("button-disabled", (e, button) => {
document.getElementById(button.id).classList.toggle("disabled", button.disabled);
});
ipcRenderer.on("input-blur", (e, input) => {
document.getElementById(input.id).blur();
});
ipcRenderer.on("input-select", (e, input) => {
document.getElementById(input.id).select();
});
ipcRenderer.on("input-text", (e, input) => {
document.getElementById(input.id).value = input.text;
document.getElementById(input.id).placeholder = input.placeholder;
});
ipcRenderer.on("data-theme", (e, data) => {
document.documentElement.setAttribute("data-theme", data.theme);
});
};
registerSendEvents();
registerReceiveEvents();
</script>
</body>
</html>
+22 -19
View File
@@ -61,11 +61,11 @@
<body>
<menu>
<li>
<button id="refresh">
<button id="keyboard">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>web-refresh</title>
<title>keyboard-close-outline</title>
<path
d="M13.82 14H9.66C9.56 13.34 9.5 12.68 9.5 12S9.56 10.65 9.66 10H14.34C14.43 10.65 14.5 11.32 14.5 12C14.5 12.5 14.46 13 14.4 13.46C15 12.96 15.72 12.57 16.5 12.32C16.5 12.21 16.5 12.11 16.5 12C16.5 11.32 16.44 10.66 16.36 10H19.74C19.9 10.64 20 11.31 20 12C20 12.06 20 12.12 20 12.18C20.7 12.35 21.35 12.63 21.95 13C22 12.68 22 12.34 22 12C22 6.5 17.5 2 12 2C6.47 2 2 6.5 2 12C2 17.5 6.5 22 12 22C12.34 22 12.68 22 13 21.95C12.59 21.29 12.29 20.55 12.13 19.75C12.09 19.82 12.05 19.89 12 19.96C11.17 18.76 10.5 17.43 10.09 16H12.5C12.81 15.25 13.26 14.58 13.82 14M18.92 8H15.97C15.65 6.75 15.19 5.55 14.59 4.44C16.43 5.07 17.96 6.34 18.92 8M12 4.03C12.83 5.23 13.5 6.57 13.91 8H10.09C10.5 6.57 11.17 5.23 12 4.03M4.26 14C4.1 13.36 4 12.69 4 12S4.1 10.64 4.26 10H7.64C7.56 10.66 7.5 11.32 7.5 12S7.56 13.34 7.64 14H4.26M5.08 16H8C8.35 17.25 8.8 18.45 9.4 19.56C7.57 18.93 6.03 17.65 5.08 16M8 8H5.08C6.03 6.34 7.57 5.06 9.4 4.44C8.8 5.55 8.35 6.75 8 8M20.83 15.67L22 14.5V18.5H18L19.77 16.73C19.32 16.28 18.69 16 18 16C16.62 16 15.5 17.12 15.5 18.5S16.62 21 18 21C18.82 21 19.54 20.61 20 20H21.71C21.12 21.47 19.68 22.5 18 22.5C15.79 22.5 14 20.71 14 18.5S15.79 14.5 18 14.5C19.11 14.5 20.11 14.95 20.83 15.67Z"
d="M12 23L16 19H8M4 3C2.9 3 2 3.9 2 5V15C2 16.1 2.9 17 4 17H20C21.1 17 22 16.1 22 15V5C22 3.9 21.1 3 20 3H4M4 5H20V15H4V5M5 6V8H7V6H5M8 6V8H10V6H8M11 6V8H13V6H11M14 6V8H16V6H14M17 6V8H19V6H17M5 9V11H7V9H5M8 9V11H10V9H8M11 9V11H13V9H11M14 9V11H16V9H14M17 9V11H19V9H17M8 12V14H16V12H8Z"
/>
</svg>
</button>
@@ -91,11 +91,11 @@
</button>
</li>
<li>
<button id="keyboard">
<button id="navigation">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>keyboard-close-outline</title>
<title>web</title>
<path
d="M12 23L16 19H8M4 3C2.9 3 2 3.9 2 5V15C2 16.1 2.9 17 4 17H20C21.1 17 22 16.1 22 15V5C22 3.9 21.1 3 20 3H4M4 5H20V15H4V5M5 6V8H7V6H5M8 6V8H10V6H8M11 6V8H13V6H11M14 6V8H16V6H14M17 6V8H19V6H17M5 9V11H7V9H5M8 9V11H10V9H8M11 9V11H13V9H11M14 9V11H16V9H14M17 9V11H19V9H17M8 12V14H16V12H8Z"
d="M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
/>
</svg>
</button>
@@ -104,22 +104,25 @@
<script>
const { ipcRenderer } = require("electron");
// Register button click send message
document.querySelectorAll("button").forEach((button) => {
button.addEventListener("click", (e) => {
ipcRenderer.send("button-click", { id: e.target.id });
const registerSendEvents = () => {
document.querySelectorAll("button").forEach((button) => {
button.addEventListener("click", (e) => {
ipcRenderer.send("button-click", { id: e.target.id });
});
});
});
};
// Register button hide receive message
ipcRenderer.on("button-hide", (e, button) => {
document.getElementById(button.id).parentNode.hidden = true;
});
const registerReceiveEvents = () => {
ipcRenderer.on("button-hidden", (e, button) => {
document.getElementById(button.id).parentNode.hidden = button.hidden;
});
ipcRenderer.on("data-theme", (e, data) => {
document.documentElement.setAttribute("data-theme", data.theme);
});
};
// Register data theme receive message
ipcRenderer.on("data-theme", (e, data) => {
document.documentElement.setAttribute("data-theme", data.theme);
});
registerSendEvents();
registerReceiveEvents();
</script>
</body>
</html>
+19 -6
View File
@@ -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();
View File
+112 -17
View File
@@ -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) }}",
+696
View File
@@ -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 = `
<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.
* @returns {string} A data string with the generated html.
*/
const errorHtml = (code, text, url) => {
const html = `
<html>
<head>
<style>
body {
display: flex;
align-items: center;
justify-content: center;
font-family: sans-serif;
text-align: center;
}
.icon {
margin: 0;
font-size: 5rem;
color: orange;
}
.title {
margin: 0;
}
</style>
</head>
<body>
<div>
<p class="icon">&#9888;</p>
<h1 class="title">Whoopsie!</h1>
<p><strong>Loading:</strong> ${url}</p>
<p><strong>Error:</strong> ${text} (${code})</p>
</div>
</body>
</html>`;
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
};
module.exports = {
init,
update,
};
-342
View File
@@ -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 = `
<html>
<body style="display:flex;justify-content:center;align-items:center;font-family:sans-serif;">
<div style="text-align:center;">
<h1>Whoopsie!</h1>
<p><strong>Loading:</strong> ${url}</p>
<p><strong>Error:</strong> ${description} (${code})</p>
</div>
</body>
</html>`;
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,
};