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>
+15 -12
View File
@@ -61,11 +61,11 @@
<body> <body>
<menu> <menu>
<li> <li>
<button id="refresh"> <button id="keyboard">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>web-refresh</title> <title>keyboard-close-outline</title>
<path <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> </svg>
</button> </button>
@@ -91,11 +91,11 @@
</button> </button>
</li> </li>
<li> <li>
<button id="keyboard"> <button id="navigation">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>keyboard-close-outline</title> <title>web</title>
<path <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> </svg>
</button> </button>
@@ -104,22 +104,25 @@
<script> <script>
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require("electron");
// Register button click send message const registerSendEvents = () => {
document.querySelectorAll("button").forEach((button) => { document.querySelectorAll("button").forEach((button) => {
button.addEventListener("click", (e) => { button.addEventListener("click", (e) => {
ipcRenderer.send("button-click", { id: e.target.id }); ipcRenderer.send("button-click", { id: e.target.id });
}); });
}); });
};
// Register button hide receive message const registerReceiveEvents = () => {
ipcRenderer.on("button-hide", (e, button) => { ipcRenderer.on("button-hidden", (e, button) => {
document.getElementById(button.id).parentNode.hidden = true; document.getElementById(button.id).parentNode.hidden = button.hidden;
}); });
// Register data theme receive message
ipcRenderer.on("data-theme", (e, data) => { ipcRenderer.on("data-theme", (e, data) => {
document.documentElement.setAttribute("data-theme", data.theme); document.documentElement.setAttribute("data-theme", data.theme);
}); });
};
registerSendEvents();
registerReceiveEvents();
</script> </script>
</body> </body>
</html> </html>
+17 -4
View File
@@ -2,9 +2,9 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const crypto = require("crypto"); const crypto = require("crypto");
const readline = require("readline/promises"); const readline = require("readline/promises");
const integration = require("./src/integration"); const integration = require("./js/integration");
const hardware = require("./src/hardware"); const hardware = require("./js/hardware");
const webview = require("./src/webview"); const webview = require("./js/webview");
const { app } = require("electron"); const { app } = require("electron");
global.ARGS = global.ARGS || {}; global.ARGS = global.ARGS || {};
@@ -44,6 +44,12 @@ app.whenReady().then(async () => {
} else if (!argsProvided && argsFileExists) { } else if (!argsProvided && argsFileExists) {
args = readArgs(argsFilePath); 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; global.ARGS = args;
// Show used arguments // Show used arguments
@@ -68,9 +74,12 @@ const parseArgs = (proc) => {
let args = {}; let args = {};
proc.argv.slice(1).forEach((arg) => { proc.argv.slice(1).forEach((arg) => {
if (arg !== ".") { if (arg !== ".") {
const [key, value] = /^(.*?)=(.*)$/.exec(arg).slice(1); const match = /^(.*?)=(.*)$/.exec(arg);
if (match) {
const [key, value] = match.slice(1);
args[key.replace("--", "").replace("-", "_")] = value; args[key.replace("--", "").replace("-", "_")] = value;
} }
}
}); });
return args; return args;
}; };
@@ -164,9 +173,13 @@ const promptArgs = async (proc) => {
const prompt = `${question} (${fallback}): `; const prompt = `${question} (${fallback}): `;
const answer = await read.question(prompt); const answer = await read.question(prompt);
const value = answer.trim() || fallback; const value = answer.trim() || fallback;
if (key === "web_url") {
args[key] = value.split(",").map((v) => v.trim());
} else {
args[key] = value; args[key] = value;
} }
} }
}
read.close(); read.close();
return args; return args;
View File
+112 -17
View File
@@ -10,7 +10,7 @@ global.INTEGRATION = global.INTEGRATION || {
/** /**
* Initializes the integration with the provided arguments. * 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 () => { const init = async () => {
if (!ARGS.mqtt_url) { if (!ARGS.mqtt_url) {
@@ -47,7 +47,7 @@ const init = async () => {
}; };
// Connection settings // 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); const masked = password === null ? "null" : "*".repeat(password.length);
console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`); console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
INTEGRATION.client = mqtt.connect(url.toString(), options); INTEGRATION.client = mqtt.connect(url.toString(), options);
@@ -64,6 +64,8 @@ const init = async () => {
initKiosk(INTEGRATION.client); initKiosk(INTEGRATION.client);
initDisplay(INTEGRATION.client); initDisplay(INTEGRATION.client);
initKeyboard(INTEGRATION.client); initKeyboard(INTEGRATION.client);
initPageNumber(INTEGRATION.client);
initPageUrl(INTEGRATION.client);
// Init client sensors // Init client sensors
initModel(INTEGRATION.client); initModel(INTEGRATION.client);
@@ -124,6 +126,8 @@ const update = async () => {
// Update client sensors // Update client sensors
updateKiosk(INTEGRATION.client); updateKiosk(INTEGRATION.client);
updatePageNumber(INTEGRATION.client);
updatePageUrl(INTEGRATION.client);
updateUpTime(INTEGRATION.client); updateUpTime(INTEGRATION.client);
updateLastActive(INTEGRATION.client); updateLastActive(INTEGRATION.client);
updateMemoryUsage(INTEGRATION.client); updateMemoryUsage(INTEGRATION.client);
@@ -203,7 +207,7 @@ const initRefresh = (client) => {
if (topic === `${root}/execute`) { if (topic === `${root}/execute`) {
console.log("Refreshing webview..."); console.log("Refreshing webview...");
hardware.setDisplayStatus("ON"); hardware.setDisplayStatus("ON");
WEBVIEW.view.webContents.reloadIgnoringCache(); WEBVIEW.views[WEBVIEW.viewActive].webContents.reloadIgnoringCache();
} }
}) })
.subscribe(`${root}/execute`); .subscribe(`${root}/execute`);
@@ -217,7 +221,7 @@ const initRefresh = (client) => {
const initKiosk = (client) => { const initKiosk = (client) => {
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`; const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
const config = { const config = {
name: `Kiosk`, name: "Kiosk",
unique_id: `${INTEGRATION.node}_kiosk`, unique_id: `${INTEGRATION.node}_kiosk`,
command_topic: `${root}/set`, command_topic: `${root}/set`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
@@ -287,7 +291,7 @@ const initDisplay = (client) => {
} }
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`; const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
const config = { const config = {
name: `Display`, name: "Display",
unique_id: `${INTEGRATION.node}_display`, unique_id: `${INTEGRATION.node}_display`,
command_topic: `${root}/set`, command_topic: `${root}/set`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
@@ -348,7 +352,7 @@ const initKeyboard = (client) => {
} }
const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`; const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`;
const config = { const config = {
name: `Keyboard`, name: "Keyboard",
unique_id: `${INTEGRATION.node}_keyboard`, unique_id: `${INTEGRATION.node}_keyboard`,
command_topic: `${root}/set`, command_topic: `${root}/set`,
state_topic: `${root}/status`, 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. * Initializes the model sensor.
* *
@@ -401,7 +496,7 @@ const updateKeyboard = (client) => {
const initModel = (client) => { const initModel = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
const config = { const config = {
name: `Model`, name: "Model",
unique_id: `${INTEGRATION.node}_model`, unique_id: `${INTEGRATION.node}_model`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ value }}", value_template: "{{ value }}",
@@ -433,7 +528,7 @@ const updateModel = (client) => {
const initSerialNumber = (client) => { const initSerialNumber = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
const config = { const config = {
name: `Serial Number`, name: "Serial Number",
unique_id: `${INTEGRATION.node}_serial_number`, unique_id: `${INTEGRATION.node}_serial_number`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ value }}", value_template: "{{ value }}",
@@ -465,7 +560,7 @@ const updateSerialNumber = (client) => {
const initHostName = (client) => { const initHostName = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
const config = { const config = {
name: `Host Name`, name: "Host Name",
unique_id: `${INTEGRATION.node}_host_name`, unique_id: `${INTEGRATION.node}_host_name`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ value }}", value_template: "{{ value }}",
@@ -497,7 +592,7 @@ const updateHostName = (client) => {
const initUpTime = (client) => { const initUpTime = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
const config = { const config = {
name: `Up Time`, name: "Up Time",
unique_id: `${INTEGRATION.node}_up_time`, unique_id: `${INTEGRATION.node}_up_time`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ (value | float) | round(0) }}", value_template: "{{ (value | float) | round(0) }}",
@@ -530,7 +625,7 @@ const updateUpTime = (client) => {
const initMemorySize = (client) => { const initMemorySize = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
const config = { const config = {
name: `Memory Size`, name: "Memory Size",
unique_id: `${INTEGRATION.node}_memory_size`, unique_id: `${INTEGRATION.node}_memory_size`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ (value | float) | round(2) }}", value_template: "{{ (value | float) | round(2) }}",
@@ -563,7 +658,7 @@ const updateMemorySize = (client) => {
const initMemoryUsage = (client) => { const initMemoryUsage = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
const config = { const config = {
name: `Memory Usage`, name: "Memory Usage",
unique_id: `${INTEGRATION.node}_memory_usage`, unique_id: `${INTEGRATION.node}_memory_usage`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ (value | float) | round(0) }}", value_template: "{{ (value | float) | round(0) }}",
@@ -596,7 +691,7 @@ const updateMemoryUsage = (client) => {
const initProcessorUsage = (client) => { const initProcessorUsage = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
const config = { const config = {
name: `Processor Usage`, name: "Processor Usage",
unique_id: `${INTEGRATION.node}_processor_usage`, unique_id: `${INTEGRATION.node}_processor_usage`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ (value | float) | round(0) }}", value_template: "{{ (value | float) | round(0) }}",
@@ -629,7 +724,7 @@ const updateProcessorUsage = (client) => {
const initProcessorTemperature = (client) => { const initProcessorTemperature = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
const config = { const config = {
name: `Processor Temperature`, name: "Processor Temperature",
unique_id: `${INTEGRATION.node}_processor_temperature`, unique_id: `${INTEGRATION.node}_processor_temperature`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ (value | float) | round(0) }}", value_template: "{{ (value | float) | round(0) }}",
@@ -662,7 +757,7 @@ const updateProcessorTemperature = (client) => {
const initPackageUpgrades = (client) => { const initPackageUpgrades = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`;
const config = { const config = {
name: `Package Upgrades`, name: "Package Upgrades",
unique_id: `${INTEGRATION.node}_package_upgrades`, unique_id: `${INTEGRATION.node}_package_upgrades`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
json_attributes_topic: `${root}/attributes`, json_attributes_topic: `${root}/attributes`,
@@ -700,7 +795,7 @@ const updatePackageUpgrades = (client) => {
const initHeartbeat = (client) => { const initHeartbeat = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
const config = { const config = {
name: `Heartbeat`, name: "Heartbeat",
unique_id: `${INTEGRATION.node}_heartbeat`, unique_id: `${INTEGRATION.node}_heartbeat`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ value }}", value_template: "{{ value }}",
@@ -732,7 +827,7 @@ const updateHeartbeat = (client) => {
const initLastActive = (client) => { const initLastActive = (client) => {
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`; const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`;
const config = { const config = {
name: `Last Active`, name: "Last Active",
unique_id: `${INTEGRATION.node}_last_active`, unique_id: `${INTEGRATION.node}_last_active`,
state_topic: `${root}/status`, state_topic: `${root}/status`,
value_template: "{{ (value | float) | round(0) }}", 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,
};