* Add Page Zoom Control Adds MQTT discovery for a "Page Zoom" input to control the `WEBVIEW.viewZoom` parameter. The MQTT value has been configured to be in % which is then converted to a scale factor when assigned or read from `WEBVIEW.viewZoom`. * Correct Type for page_zoom publishConfig Accidentally replaced "number" with "zoom" when changing paramters. * Add updateZoom Functionality to webview Add a setter function for the viewZoom property that updates the zoom factor for all views when the property is changed.
729 lines
19 KiB
JavaScript
729 lines
19 KiB
JavaScript
const path = require("path");
|
|
const hardware = require("./hardware");
|
|
const integration = require("./integration");
|
|
const { app, screen, nativeTheme, ipcMain, session, 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();
|
|
}
|
|
session.defaultSession.clearCache();
|
|
|
|
// Parse arguments
|
|
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;
|
|
Object.defineProperty(WEBVIEW, "viewZoom", {
|
|
get() {
|
|
return this._viewZoom || zoom;
|
|
},
|
|
set(v) {
|
|
this._viewZoom = Math.max(0, v);
|
|
updateZoom();
|
|
},
|
|
});
|
|
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();
|
|
|
|
// Hide pager button
|
|
WEBVIEW.navigation.webContents.send("button-hidden", {
|
|
id: "pager",
|
|
hidden: WEBVIEW.viewUrls.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 });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Updates zoom level for all webviews.
|
|
*/
|
|
const updateZoom = () => {
|
|
// And apply to all views
|
|
WEBVIEW.views.forEach((view) => {
|
|
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
|
});
|
|
|
|
};
|
|
|
|
/**
|
|
* 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 now = new Date();
|
|
const then = WEBVIEW.pointer.time;
|
|
const delta = Math.abs(now - then) / 1000;
|
|
const posNew = screen.getCursorScreenPoint();
|
|
const posOld = WEBVIEW.pointer.position;
|
|
|
|
// Cursor movement detected
|
|
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
|
WEBVIEW.pointer.time = now;
|
|
|
|
// Update integration sensor
|
|
if (delta > 30) {
|
|
console.log("Update Last Active");
|
|
integration.update();
|
|
}
|
|
} else if (delta > 60) {
|
|
const navigation = WEBVIEW.navigation.getBounds();
|
|
|
|
// Auto-hide navigation
|
|
if (navigation.height > 0) {
|
|
toggleNavigation();
|
|
}
|
|
}
|
|
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">⚠</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,
|
|
};
|