feat: remember zoom and theme per page #108

This commit is contained in:
leukipp
2025-11-10 11:25:53 +01:00
parent b3a787d03d
commit a3860ebd69
2 changed files with 188 additions and 83 deletions
+9 -8
View File
@@ -104,9 +104,14 @@ const init = async () => {
// Register global events // Register global events
EVENTS.on("updateApp", updateApp); EVENTS.on("updateApp", updateApp);
EVENTS.on("updateStatus", updateKiosk); EVENTS.on("updateStatus", updateKiosk);
EVENTS.on("updateTheme", updateTheme);
EVENTS.on("updateVolume", updateVolume); EVENTS.on("updateVolume", updateVolume);
EVENTS.on("updateKeyboard", updateKeyboard); EVENTS.on("updateKeyboard", updateKeyboard);
EVENTS.on("updatePage", () => {
updatePageNumber();
updatePageZoom();
updatePageUrl();
updateTheme();
});
EVENTS.on("updateDisplay", () => { EVENTS.on("updateDisplay", () => {
updateDisplay(); updateDisplay();
updateLastActive(); updateLastActive();
@@ -155,10 +160,6 @@ const update = async () => {
} }
console.debug("integration.js: update()"); console.debug("integration.js: update()");
// Update client sensors
updatePageNumber();
updatePageZoom();
updatePageUrl();
updateNetworkAddress(); updateNetworkAddress();
updateUpTime(); updateUpTime();
updateLastActive(); updateLastActive();
@@ -424,7 +425,7 @@ const initTheme = () => {
publishConfig("select", config) publishConfig("select", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
const theme = message.toString(); const theme = message.toString().toLowerCase();
console.verbose("Set Application Theme:", theme); console.verbose("Set Application Theme:", theme);
WEBVIEW.theme.set(theme); WEBVIEW.theme.set(theme);
} }
@@ -654,7 +655,7 @@ const initPageZoom = () => {
const zoom = parseInt(message, 10); const zoom = parseInt(message, 10);
if (WEBVIEW.viewActive && zoom) { if (WEBVIEW.viewActive && zoom) {
console.verbose("Set Page Zoom:", zoom); console.verbose("Set Page Zoom:", zoom);
WEBVIEW.views[WEBVIEW.viewActive].webContents.setZoomFactor(zoom / 100.0); WEBVIEW.zoom.set(zoom);
EVENTS.emit("updateView"); EVENTS.emit("updateView");
} }
} }
@@ -667,7 +668,7 @@ const initPageZoom = () => {
* Updates the page zoom via the mqtt connection. * Updates the page zoom via the mqtt connection.
*/ */
const updatePageZoom = async () => { const updatePageZoom = async () => {
const pageZoom = Math.round(WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getZoomFactor() * 100.0); const pageZoom = WEBVIEW.zoom.get();
publishState("page_zoom", pageZoom); publishState("page_zoom", pageZoom);
}; };
+179 -75
View File
@@ -46,24 +46,31 @@ const init = async () => {
console.error("Please provide the '--web-url' parameter with http(s)"); console.error("Please provide the '--web-url' parameter with http(s)");
return app.quit(); return app.quit();
} }
session.defaultSession.clearCache();
// Clear cache and storage
await session.defaultSession.clearCache();
if (ARGS.app_reset === "storage") {
await session.defaultSession.clearStorageData();
}
// Parse arguments // Parse arguments
const debug = "app_debug" in ARGS; const debug = "app_debug" in ARGS;
const widget = ARGS.web_widget ? ARGS.web_widget === "true" : 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 theme = ["light", "dark"].includes(ARGS.web_theme) ? ARGS.web_theme : "dark";
const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url); const zoom = (!isNaN(parseFloat(ARGS.web_zoom)) ? parseFloat(ARGS.web_zoom) : 1.25) * 100;
const urls = [loaderHtml(40, 1.0, theme), ...ARGS.web_url];
// Init global properties // Init global controls
WEBVIEW.viewActive = 0;
WEBVIEW.viewUrls = urls;
WEBVIEW.viewZoom = zoom;
WEBVIEW.statusEnabled = !debug; WEBVIEW.statusEnabled = !debug;
WEBVIEW.pagerEnabled = widget; WEBVIEW.pagerEnabled = widget;
WEBVIEW.widgetEnabled = widget; WEBVIEW.widgetEnabled = widget;
WEBVIEW.navigationEnabled = widget; WEBVIEW.navigationEnabled = widget;
// Init global views
WEBVIEW.views = [];
WEBVIEW.viewUrls = urls;
WEBVIEW.viewActive = 0;
// Init global display // Init global display
const display = screen.getPrimaryDisplay(); const display = screen.getPrimaryDisplay();
WEBVIEW.display = display ? display.workAreaSize : {}; WEBVIEW.display = display ? display.workAreaSize : {};
@@ -71,25 +78,63 @@ const init = async () => {
WEBVIEW.display = { width: 800, height: 600 }; WEBVIEW.display = { width: 800, height: 600 };
} }
// Init global theme // Init global theme manager
WEBVIEW.theme = { WEBVIEW.theme = {
set: (name) => { set: function (value) {
nativeTheme.themeSource = name.toLowerCase(); const theme = value.toLowerCase();
nativeTheme.themeSource = theme;
if (typeof this.callback === "function") this.callback();
}, },
get: () => { get: function () {
return nativeTheme.shouldUseDarkColors ? "dark" : "light"; return nativeTheme.shouldUseDarkColors ? "dark" : "light";
}, },
toggle: () => { toggle: function () {
nativeTheme.themeSource = nativeTheme.shouldUseDarkColors ? "light" : "dark"; this.set(nativeTheme.shouldUseDarkColors ? "light" : "dark");
}, },
handler: (callback) => { reset: function () {
nativeTheme.on("updated", callback); this.set(this.default);
},
store: function () {
cookieStore("web-theme", this.get());
},
init: function (value, callback) {
this.callback = callback;
this.default = value;
this.reset();
}, },
}; };
WEBVIEW.theme.handler(updateTheme);
WEBVIEW.theme.set(theme);
// Init global root window // Init global zoom manager
WEBVIEW.zoom = {
set: function (value) {
const zoom = Math.max(0.25, Math.min(4.0, value / 100.0));
WEBVIEW.views[WEBVIEW.viewActive].webContents.setZoomFactor(zoom);
if (typeof this.callback === "function") this.callback();
},
get: function () {
const zoom = WEBVIEW.views[WEBVIEW.viewActive].webContents.getZoomFactor();
return Math.round(zoom * 100.0);
},
minus: function () {
this.set(Math.floor((this.get() - 1) / 10) * 10);
},
plus: function () {
this.set(Math.ceil((this.get() + 1) / 10) * 10);
},
reset: function () {
this.set(this.default);
},
store: function () {
cookieStore("web-zoom", this.get());
},
init: function (value, callback) {
this.callback = callback;
this.default = value;
this.reset();
},
};
// Init root window
WEBVIEW.window = new BaseWindow({ WEBVIEW.window = new BaseWindow({
title: APP.title, title: APP.title,
icon: APP.icon, icon: APP.icon,
@@ -100,13 +145,9 @@ const init = async () => {
minWidth: 136, minWidth: 136,
minHeight: 136, minHeight: 136,
}); });
const size = WEBVIEW.window.getBounds();
console.info(`Open Window: ${size.width}x${size.height}+${size.x}+${size.y}`);
updateTheme();
// Init global webview // Init global webview
WEBVIEW.views = []; WEBVIEW.viewUrls.forEach((url, i) => {
urls.forEach((url, i) => {
const view = new WebContentsView({ const view = new WebContentsView({
webPreferences: { webPreferences: {
nodeIntegration: false, nodeIntegration: false,
@@ -170,6 +211,12 @@ const init = async () => {
WEBVIEW.window.contentView.addChildView(WEBVIEW.navigation); WEBVIEW.window.contentView.addChildView(WEBVIEW.navigation);
WEBVIEW.navigation.webContents.loadFile(path.join(APP.path, "html", "navigation.html")); WEBVIEW.navigation.webContents.loadFile(path.join(APP.path, "html", "navigation.html"));
// Init global layout
const { width, height, x, y } = WEBVIEW.window.getBounds();
console.info(`Open Window: ${width}x${height}+${x}+${y}`);
WEBVIEW.theme.init(theme, updateTheme);
WEBVIEW.zoom.init(zoom, updateZoom);
// Register global events // Register global events
await windowEvents(); await windowEvents();
await widgetEvents(); await widgetEvents();
@@ -201,9 +248,6 @@ const update = async () => {
// Update navigation status // Update navigation status
updateNavigation(); updateNavigation();
// Update integration sensor
integration.update();
}; };
/** /**
@@ -213,24 +257,39 @@ const updateTheme = () => {
if (APP.exiting) { if (APP.exiting) {
return; return;
} }
const previous = WEBVIEW.statusTheme;
const theme = WEBVIEW.theme.get(); const theme = WEBVIEW.theme.get();
// Set controls theme // Update controls theme
if (previous !== theme) {
console.info("Update Application Theme:", theme);
}
WEBVIEW.statusTheme = theme; WEBVIEW.statusTheme = theme;
WEBVIEW.pagerTheme = theme; WEBVIEW.pagerTheme = theme;
WEBVIEW.widgetTheme = theme; WEBVIEW.widgetTheme = theme;
WEBVIEW.navigationTheme = theme; WEBVIEW.navigationTheme = theme;
// Update controls theme
WEBVIEW.window.setMenuBarVisibility(false); WEBVIEW.window.setMenuBarVisibility(false);
// Update application theme
WEBVIEW.theme.store();
if (WEBVIEW.initialized) { if (WEBVIEW.initialized) {
console.info("Update Application Theme:", theme);
resizeView(); resizeView();
} }
EVENTS.emit("updateTheme"); EVENTS.emit("updatePage");
};
/**
* Updates the page zoom.
*/
const updateZoom = () => {
if (APP.exiting) {
return;
}
const zoom = WEBVIEW.zoom.get();
// Update page zoom
WEBVIEW.zoom.store();
if (WEBVIEW.initialized) {
console.info("Update Page Zoom:", zoom);
}
EVENTS.emit("updatePage");
}; };
/** /**
@@ -240,7 +299,10 @@ const updateView = () => {
if (!WEBVIEW.viewActive) { if (!WEBVIEW.viewActive) {
return; return;
} }
const url = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL(); const view = WEBVIEW.views[WEBVIEW.viewActive];
const url = view.webContents.getURL();
// Build window title
const host = url.startsWith("data:") ? "whoopsie" : new URL(url).host; const host = url.startsWith("data:") ? "whoopsie" : new URL(url).host;
const title = `${APP.title} - ${host} (${WEBVIEW.viewActive})`; const title = `${APP.title} - ${host} (${WEBVIEW.viewActive})`;
const previous = WEBVIEW.window.getTitle(); const previous = WEBVIEW.window.getTitle();
@@ -252,10 +314,23 @@ const updateView = () => {
WEBVIEW.status.webContents.send("text-content", { id: "title", content: title }); WEBVIEW.status.webContents.send("text-content", { id: "title", content: title });
WEBVIEW.window.setTitle(title); WEBVIEW.window.setTitle(title);
// Update stored theme and zoom
cookieStore("web-theme").then((value) => {
if (value && value !== WEBVIEW.theme.get()) {
WEBVIEW.theme.set(value);
}
});
cookieStore("web-zoom").then((value) => {
if (value && value !== WEBVIEW.zoom.get()) {
WEBVIEW.zoom.set(value);
}
});
// Hide all other webviews and show only the active one // Hide all other webviews and show only the active one
WEBVIEW.views.forEach((view, i) => { WEBVIEW.views.forEach((view, i) => {
view.setVisible(i === WEBVIEW.viewActive); view.setVisible(i === WEBVIEW.viewActive);
}); });
EVENTS.emit("updatePage");
update(); update();
}; };
@@ -360,11 +435,11 @@ const updateNavigation = () => {
// Disable zoom buttons // Disable zoom buttons
WEBVIEW.navigation.webContents.send("button-disabled", { WEBVIEW.navigation.webContents.send("button-disabled", {
id: "minus", id: "minus",
disabled: view.webContents.getZoomFactor().toFixed(2) <= 0.25, disabled: WEBVIEW.zoom.get() <= 25,
}); });
WEBVIEW.navigation.webContents.send("button-disabled", { WEBVIEW.navigation.webContents.send("button-disabled", {
id: "plus", id: "plus",
disabled: view.webContents.getZoomFactor().toFixed(2) >= 4.0, disabled: WEBVIEW.zoom.get() >= 400,
}); });
// Disable history buttons // Disable history buttons
@@ -446,10 +521,8 @@ const zoomMinus = () => {
if (!WEBVIEW.viewActive) { if (!WEBVIEW.viewActive) {
return; return;
} }
const view = WEBVIEW.views[WEBVIEW.viewActive]; WEBVIEW.zoom.minus();
const zoom = view.webContents.getZoomFactor() - 0.1; updateView();
view.webContents.setZoomFactor(Math.max(0.25, Math.round(zoom * 10) / 10));
update();
}; };
/** /**
@@ -459,10 +532,8 @@ const zoomPlus = () => {
if (!WEBVIEW.viewActive) { if (!WEBVIEW.viewActive) {
return; return;
} }
const view = WEBVIEW.views[WEBVIEW.viewActive]; WEBVIEW.zoom.plus();
const zoom = view.webContents.getZoomFactor() + 0.1; updateView();
view.webContents.setZoomFactor(Math.min(4.0, Math.round(zoom * 10) / 10));
update();
}; };
/** /**
@@ -528,8 +599,14 @@ const homeView = () => {
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
const currentUrl = view.webContents.getURL(); const currentUrl = view.webContents.getURL();
// Reset log history // Clear logs, cache and history
APP.logs = []; APP.logs = [];
view.webContents.session.clearCache();
view.webContents.navigationHistory.clear();
// Reset page zoom and theme
WEBVIEW.zoom.reset();
WEBVIEW.theme.reset();
// Reload the default url or refresh the page // Reload the default url or refresh the page
if (currentUrl != defaultUrl) { if (currentUrl != defaultUrl) {
@@ -537,13 +614,6 @@ const homeView = () => {
} else { } else {
view.webContents.reloadIgnoringCache(); view.webContents.reloadIgnoringCache();
} }
// Reset page zoom and history
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
setTimeout(() => {
view.webContents.navigationHistory.clear();
update();
}, 2000);
}; };
/** /**
@@ -557,8 +627,9 @@ const reloadView = () => {
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
const currentUrl = view.webContents.getURL(); const currentUrl = view.webContents.getURL();
// Reset log history // Clear logs and cache
APP.logs = []; APP.logs = [];
view.webContents.session.clearCache();
// Reload the default url or refresh the page // Reload the default url or refresh the page
if (currentUrl.startsWith("data:")) { if (currentUrl.startsWith("data:")) {
@@ -566,7 +637,6 @@ const reloadView = () => {
} else { } else {
view.webContents.reloadIgnoringCache(); view.webContents.reloadIgnoringCache();
} }
update();
}; };
/** /**
@@ -692,6 +762,7 @@ const windowEvents = async () => {
} }
break; break;
case "Terminated": case "Terminated":
process.exitCode = 0;
app.quit(); app.quit();
break; break;
} }
@@ -709,6 +780,7 @@ const windowEvents = async () => {
// Handle window resize events // Handle window resize events
WEBVIEW.window.on("resize", resizeView); WEBVIEW.window.on("resize", resizeView);
resizeView();
// Handle global shortcut events // Handle global shortcut events
globalShortcut.register("Control+Left", () => { globalShortcut.register("Control+Left", () => {
@@ -959,15 +1031,14 @@ const navigationEvents = async () => {
*/ */
const viewEvents = async () => { const viewEvents = async () => {
const ready = []; const ready = [];
const debug = "app_debug" in ARGS;
const loaded = (i) => { const loaded = (i) => {
if (WEBVIEW.viewActive !== 0) { if (WEBVIEW.viewActive) {
return true; return true;
} }
// Set window status to fullscreen // Set window status to fullscreen
if (i === 0 && !debug) { if (i === 0 && !("app_debug" in ARGS)) {
WEBVIEW.window.setStatus("Fullscreen"); WEBVIEW.window.setStatus("Fullscreen");
} }
@@ -998,19 +1069,22 @@ const viewEvents = async () => {
}); });
// Update webview layout // Update webview layout
view.webContents.on("dom-ready", () => {
console.debug(`webview.js: viewEvents(${i},dom-ready)`);
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
});
view.webContents.once("dom-ready", () => { view.webContents.once("dom-ready", () => {
view.webContents.setZoomFactor(WEBVIEW.viewZoom); console.debug(`webview.js: viewEvents(${i},dom-ready)`);
if ("app_reset" in ARGS) {
cookieStore("web-theme", WEBVIEW.theme.default, view);
cookieStore("web-zoom", WEBVIEW.zoom.default, view);
}
ready.push(i); ready.push(i);
}); });
view.webContents.on("dom-ready", () => {
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
});
// Webview fully loaded // Webview fully loaded
view.webContents.on("did-finish-load", () => { view.webContents.on("did-finish-load", () => {
console.debug(`webview.js: viewEvents(${i},did-finish-load)`); console.debug(`webview.js: viewEvents(${i},did-finish-load)`);
if (debug) { if ("app_debug" in ARGS) {
setTimeout(() => { setTimeout(() => {
view.webContents.openDevTools(); view.webContents.openDevTools();
}, 2000); }, 2000);
@@ -1035,16 +1109,16 @@ const viewEvents = async () => {
}); });
// Webview url changed // Webview url changed
view.webContents.on("did-navigate", () => {
console.debug(`webview.js: viewEvents(${i},did-navigate)`);
updateView();
});
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => { view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
if (mainframe) { if (mainframe) {
console.debug(`webview.js: viewEvents(${i},did-navigate-in-page)`); console.debug(`webview.js: viewEvents(${i},did-navigate-in-page)`);
updateView(); updateView();
} }
}); });
view.webContents.on("did-navigate", () => {
console.debug(`webview.js: viewEvents(${i},did-navigate)`);
updateView();
});
// Handle webview mouse events // Handle webview mouse events
view.webContents.on("before-mouse-event", (e, mouse) => { view.webContents.on("before-mouse-event", (e, mouse) => {
@@ -1154,13 +1228,6 @@ const appEvents = async () => {
console.error(`${details.type} Process ${details.reason} (code ${details.exitCode}): ${name}`); console.error(`${details.type} Process ${details.reason} (code ${details.exitCode}): ${name}`);
}); });
// Handle exit events
app.on("before-quit", () => {
APP.exiting = true;
console.warn(`${APP.title} Terminated`);
});
process.on("SIGINT", app.quit);
// Check latest release (2h) // Check latest release (2h)
setInterval(() => { setInterval(() => {
if (APP.exiting) { if (APP.exiting) {
@@ -1234,6 +1301,43 @@ const onlineStatus = (url, interval = 1000, timeout = 60000) => {
}); });
}; };
/**
* Stores, retrieves or deletes a cookie from the active webview.
*
* @param {string} key - The key of the webview cookie.
* @param {string|number} value - The value of the webview cookie.
* @param {WebContentsView} view - The webview that stores the cookie.
* @returns {Promise<string|number>} The value of the webview cookie.
*/
const cookieStore = async (key, value, view = WEBVIEW.views[WEBVIEW.viewActive]) => {
const url = view ? view.webContents.getURL() : null;
if (!url || url.startsWith("data:")) {
return;
}
// Use origin session cookies
const name = `${APP.name}-${key}`;
const origin = new URL(url).origin;
const cookies = view.webContents.session.cookies;
const expires = Math.floor(Date.now() / 1000) + 3600 * 24 * 365;
// Delete cookie if value is null
if (value === null) {
await cookies.remove(origin, name);
return null;
}
// Write cookie if value is provided
if (value !== undefined) {
await cookies.set({ url: origin, name: name, value: `${value}`, expirationDate: expires });
return value;
}
// Read cookie if no value is provided
const cookie = (await cookies.get({ url: origin, name: name }))[0] || {};
return !isNaN(Number(cookie.value)) ? Number(cookie.value) : cookie.value;
};
/** /**
* Generates a html template for a spinning loader. * Generates a html template for a spinning loader.
* *