diff --git a/app/engine.js b/app/engine.js index 1991875..29d557d 100644 --- a/app/engine.js +++ b/app/engine.js @@ -3668,17 +3668,35 @@ $('copyAddr').addEventListener('click', () => { * is made in their own design tool. The built-in poster page is still there * at /poster for anyone who wants it; it is just not what this button does. */ -$('saveQr').addEventListener('click', () => { +$('saveQr').addEventListener('click', async () => { const base = shareBase(); if (!base) { log('no address to encode yet', 'err'); return; } - const a = document.createElement('a'); - a.href = 'http://127.0.0.1:' + state.local.port + '/qr.svg?url=' + - encodeURIComponent(base + '/'); - a.download = 'church-translate-qr.svg'; - document.body.appendChild(a); - a.click(); - a.remove(); - log('saved the QR for ' + base + '/', 'ok'); + /* + * Fetch the code and hand the browser a BLOB rather than linking straight at + * the server. This window is served from the asset port and the QR comes off + * the congregation port, so a plain is CROSS-ORIGIN — the + * download attribute is ignored and the window navigates to the image + * instead. With no back button, no reload item and no address bar, that + * strands the operator on a full-screen QR with no way out; build 123 did + * exactly that to a booth Mac. A blob: URL is same-origin, so it downloads. + */ + const url = 'http://127.0.0.1:' + state.local.port + '/qr.svg?url=' + + encodeURIComponent(base + '/'); + try { + const r = await fetch(url, { cache: 'no-store' }); + if (!r.ok) throw new Error('the server answered ' + r.status); + const href = URL.createObjectURL(await r.blob()); + const a = document.createElement('a'); + a.href = href; + a.download = 'church-translate-qr.svg'; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(() => URL.revokeObjectURL(href), 10000); + log('saved the QR for ' + base + '/ to Downloads', 'ok'); + } catch (err) { + log('could not save the QR: ' + err.message, 'err'); + } }); $('openOperator').addEventListener('click', () => { window.open('http://127.0.0.1:' + state.local.port + '/operator', '_blank'); diff --git a/app/main.js b/app/main.js index 4ac47ed..c1643cb 100644 --- a/app/main.js +++ b/app/main.js @@ -662,6 +662,27 @@ function createWindow() { : '/index.html'; win.loadURL(assetOrigin + page); + /* + * NOTHING replaces the app in this window. There is no back button, no + * reload item and no address bar, so a window that leaves the app page is a + * window the operator cannot get back from — which is precisely how build + * 123 stranded a booth Mac on a full-screen QR image. Our own pages still + * open as their OWN windows through setWindowOpenHandler below; this only + * refuses to replace the app itself, and sends anything external to the + * browser where it belongs. + */ + win.webContents.on('will-navigate', (e, url) => { + if (url === assetOrigin + page) return; /* reloading ourselves is fine */ + e.preventDefault(); + console.error('[main] refused to navigate the app window to ' + url); + let local = false; + try { + const u = new URL(url); + local = (u.hostname === '127.0.0.1' || u.hostname === 'localhost'); + } catch (_) { local = false; } + if (!local) shell.openExternal(url).catch(() => {}); + }); + win.webContents.on('did-finish-load', () => console.log('[main] page loaded')); win.webContents.on('did-fail-load', (_e, code, desc, url) => console.error('[main] page FAILED to load: ' + code + ' ' + desc + ' ' + url)); diff --git a/app/package.json b/app/package.json index 0ccdca8..1355148 100644 --- a/app/package.json +++ b/app/package.json @@ -2,7 +2,7 @@ "name": "church-translate-engine", "productName": "Church Translate", "version": "3.23.2", - "buildNumber": 123, + "buildNumber": 124, "private": true, "description": "Live sermon translation — self-contained: captures audio, transcribes, translates, and serves the congregation page itself", "main": "main.js", @@ -106,7 +106,7 @@ ] }, "asar": false, - "buildVersion": "123", + "buildVersion": "124", "win": { "target": [ {