diff --git a/Readme.md b/Readme.md index aad83cb..2fad438 100644 --- a/Readme.md +++ b/Readme.md @@ -195,7 +195,7 @@ The kiosk machine still needs a working internet connection (ethernet, or WiFi c - **Navigation menu** - Quick access to all sites via key icon (top-left hot corner) - **Local files** - Enter an absolute path (e.g. `/home/kiosk/docs/menu.pdf`) instead of a web address and it's shown from the kiosk's own disk: - HTML pages, PDFs (built-in viewer), and images. Works offline. The file + HTML pages, PDFs (built-in viewer), images, and Word documents (below). Works offline. The file must be readable by the `kiosk` user. - **Image slideshow** - Enter a folder instead (e.g. `/home/kiosk/photos/`) and its images (jpg, png, gif, webp, bmp, svg, avif) play full-screen in @@ -205,6 +205,13 @@ The kiosk machine still needs a working internet connection (ethernet, or WiFi c `{"interval": 10, "shuffle": false, "fit": "contain", "transition": 1, "recursive": false}` (`fit`: `contain` shows the whole image, `cover` fills the screen and crops; `recursive` includes subfolders). +- **Word documents** - A `.docx` path (e.g. `/home/kiosk/docs/menu.docx`) + is rendered as pages (text, tables, images, lists), scaled to the screen + width, via the bundled [docx-preview](https://github.com/VolodymyrBaydalka/docxjs) + library - no LibreOffice needed, works offline. Replacing the file on + disk updates the kiosk within a minute. Old `.doc` and LibreOffice + `.odt` aren't supported; save those as `.docx` or PDF. Layout is close + to Word's but not pixel-exact - for exact layout, export to PDF. ### Touch Controls - **2-finger horizontal swipe** - Switch between sites @@ -1467,6 +1474,7 @@ full migration pass. - **All-in-one install ISO** (`iso/build-iso.sh`, or Actions → Build kiosk ISO): Ubuntu Server 26.04 plus the kiosk in one USB stick — the Ubuntu install, then `install.sh` automatically on first boot. Interactive, unattended, or fully automatic. See "All-in-One Install ISO" above. - **Offline installs** (`--offline`): the ISO carries every package (including CUPS printing and all printer drivers), Node.js 22, and prebuilt Electron/Web UI modules, so a kiosk installs with no internet. With internet it still installs current versions online. Addons → CUPS Printing (menu or Web UI) installs from the bundle by itself when offline. - **Local files as sites:** an absolute path (`/home/kiosk/docs/menu.pdf`) shows a local page, PDF or image; a folder (`/home/kiosk/photos/`) plays as a full-screen **image slideshow** that picks up added/removed images within a minute. Works in both the Sites menu and the Web UI. +- **Word documents as sites:** a `.docx` path renders in the kiosk via docx-preview (~1 MB of JavaScript, bundled offline), updating when the file changes. - **Electron upgrade:** v42.x → v44.x (42 leaves Electron's support window when 45 ships). Existing kiosks move to it via Advanced → Upgrade. - **Fix:** the CUPS menu could report an installed CUPS as "not installed" (`dpkg -l | grep -q` failing under `set -o pipefail`). diff --git a/kiosk-app/docview.html b/kiosk-app/docview.html new file mode 100644 index 0000000..a28f805 --- /dev/null +++ b/kiosk-app/docview.html @@ -0,0 +1,106 @@ + + + + +Document + + + + + + +
Loading document…
+
+ + + diff --git a/kiosk-app/docview.js b/kiosk-app/docview.js new file mode 100644 index 0000000..668d67c --- /dev/null +++ b/kiosk-app/docview.js @@ -0,0 +1,82 @@ +// 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 }; diff --git a/kiosk-app/main.js b/kiosk-app/main.js index d0acd51..84deff2 100644 --- a/kiosk-app/main.js +++ b/kiosk-app/main.js @@ -4,8 +4,11 @@ const fs=require('fs'); const path=require('path'); const os=require('os'); const crypto=require('crypto'); -// Sites that are a local folder show as an image slideshow (slideshow.js). +// Local-file sites with their own viewer pages: a folder plays as an image +// slideshow (slideshow.js), a .docx renders as a Word document (docview.js). const slideshow=require('./slideshow'); +const docview=require('./docview'); +function resolveLocalSite(url){return docview.resolveSiteUrl(slideshow.resolveSiteUrl(url));} // Suppress EPIPE errors (happen when no terminal attached) process.stdout.on('error',(e)=>{if(e.code!=='EPIPE')throw e;}); @@ -784,7 +787,7 @@ function attachView(i){ const tabIdx=viewIndexToTabIndex(i); if(tabIdx>=0&&tabs[tabIdx]){ - const configuredUrl=slideshow.resolveSiteUrl(tabs[tabIdx].url); + const configuredUrl=resolveLocalSite(tabs[tabIdx].url); const currentUrl=views[i].webContents.getURL(); if(currentUrl&&!currentUrl.startsWith(configuredUrl)){ @@ -1336,7 +1339,7 @@ async function createWindow(){ url=u.toString(); }catch(e){} } - url=slideshow.resolveSiteUrl(url); + url=resolveLocalSite(url); const initialOrigin=new URL(t.url).origin; @@ -1371,6 +1374,7 @@ async function createWindow(){ view.webContents.setAudioMuted(false); slideshow.attach(view.webContents); + docview.attach(view.webContents); view.webContents.loadURL(url); view.webContents.on('did-finish-load',()=>{ diff --git a/kiosk-app/package.json b/kiosk-app/package.json index 0c2ca6c..c472b2b 100644 --- a/kiosk-app/package.json +++ b/kiosk-app/package.json @@ -3,6 +3,8 @@ "version": "1.0.0", "main": "main.js", "dependencies": { - "electron": "^44.0.0" + "electron": "^44.0.0", + "docx-preview": "^0.4.1", + "jszip": "^3.10.1" } } diff --git a/menus/sites.sh b/menus/sites.sh index 06f27e4..b47660b 100644 --- a/menus/sites.sh +++ b/menus/sites.sh @@ -137,8 +137,8 @@ action_add_page() { echo " -1 = hidden (PIN-gated, F10 or 3-finger swipe)" echo echo "URL: a web address, or a path on this kiosk - a file (page, PDF," - echo "image) like /home/kiosk/docs/menu.pdf, or a folder of images ending" - echo "in / like /home/kiosk/photos/ for a slideshow." + echo "image, Word .docx) like /home/kiosk/docs/menu.pdf, or a folder of" + echo "images ending in / like /home/kiosk/photos/ for a slideshow." local raw_url url dur name needs_auth user pass read -r -p "URL: " raw_url