A site whose path is a local .docx renders as pages in the kiosk instead of Chromium offering a download. kiosk-app/docview.js maps the file URL to docview.html (alongside slideshow.js, through main.js's new resolveLocalSite) and pushes the file's bytes into the page from the main process; the page renders them with docx-preview + jszip, both pure-JS npm dependencies of kiosk-app (so online installs, Advanced -> Upgrade and the offline bundle all pick them up - no LibreOffice). - Scaled to the screen width (capped at 160%), scrolls normally. - Re-checked every minute; a changed file re-renders from the top. - Word's Symbol/Wingdings private-use bullet glyphs (U+F0B7 etc.), which render blank without Microsoft fonts, are mapped to Unicode (•, ▪, ...). - Missing/unreadable/oversized (>50 MB) files and a missing library show a message instead of a blank page. .doc/.odt aren't supported. Tested with the real main.js on Electron 44.5.1 under Xvfb + Openbox: heading, coloured bold run, image, bordered table, bullets and a page break rendered; wheel scrolling works; replacing the file while running re-rendered the new document within the refresh. Offline bundle rebuilt and contains docx-preview and jszip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
83 lines
3.1 KiB
JavaScript
83 lines
3.1 KiB
JavaScript
// Word document (.docx) support for main.js.
|
|
//
|
|
// A site whose URL is a local .docx file (file:///home/kiosk/docs/menu.docx,
|
|
// entered in the Sites menu or Web UI as /home/kiosk/docs/menu.docx) is
|
|
// rendered with docx-preview (an npm dependency of this app - pure
|
|
// JavaScript, works offline) instead of Chromium offering to download it.
|
|
// main.js swaps the file URL for docview.html via resolveSiteUrl(), then
|
|
// attach() hands the page the file's bytes - the page itself has no
|
|
// filesystem access (contextIsolation, no Node). The file is re-checked
|
|
// every minute and re-rendered if it changed, so replacing the document
|
|
// on disk updates the kiosk without a restart.
|
|
//
|
|
// Only .docx (Word 2007+). Old binary .doc and LibreOffice .odt aren't
|
|
// supported by docx-preview - save those as .docx or PDF.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { fileURLToPath, pathToFileURL } = require('url');
|
|
|
|
const VIEWER_PAGE = path.join(__dirname, 'docview.html');
|
|
const REFRESH_MS = 60000;
|
|
const MAX_BYTES = 50 * 1024 * 1024;
|
|
|
|
// The .docx path a site URL points at, or null.
|
|
function docxPath(url) {
|
|
if (typeof url !== 'string' || !url.startsWith('file://')) return null;
|
|
let p;
|
|
try {
|
|
p = fileURLToPath(url);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
return path.extname(p).toLowerCase() === '.docx' ? p : null;
|
|
}
|
|
|
|
// What a view should actually load for a configured site URL.
|
|
function resolveSiteUrl(url) {
|
|
const file = docxPath(url);
|
|
if (!file) return url;
|
|
const u = pathToFileURL(VIEWER_PAGE);
|
|
u.searchParams.set('file', file);
|
|
return u.href;
|
|
}
|
|
|
|
function readDoc(file) {
|
|
try {
|
|
const st = fs.statSync(file);
|
|
if (st.size > MAX_BYTES) return { file, error: `Document is too large to show (${Math.round(st.size / 1048576)} MB).` };
|
|
return { file, mtime: st.mtimeMs, data: fs.readFileSync(file).toString('base64') };
|
|
} catch (e) {
|
|
return { file, error: e.code === 'ENOENT' ? `File not found: ${file}` : `Can't read ${file}: ${e.message}` };
|
|
}
|
|
}
|
|
|
|
// Feed a view's viewer page the document now and again whenever the file
|
|
// changes. Call once per view; safe on views that never show a document.
|
|
function attach(webContents) {
|
|
let lastKey = '';
|
|
const send = (force) => {
|
|
if (webContents.isDestroyed()) return;
|
|
const current = webContents.getURL();
|
|
if (!current.startsWith(pathToFileURL(VIEWER_PAGE).href)) return;
|
|
const file = new URL(current).searchParams.get('file');
|
|
if (!file) return;
|
|
let key;
|
|
try {
|
|
const st = fs.statSync(file);
|
|
key = `${st.mtimeMs}:${st.size}`;
|
|
} catch (e) {
|
|
key = 'missing';
|
|
}
|
|
if (!force && key === lastKey) return;
|
|
lastKey = key;
|
|
webContents.executeJavaScript(`window.kioskDocView&&window.kioskDocView.show(${JSON.stringify(readDoc(file))})`)
|
|
.catch(() => {});
|
|
};
|
|
webContents.on('did-finish-load', () => send(true));
|
|
const timer = setInterval(() => send(false), REFRESH_MS);
|
|
webContents.once('destroyed', () => clearInterval(timer));
|
|
}
|
|
|
|
module.exports = { resolveSiteUrl, docxPath, attach };
|