Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3891a027f3 | ||
|
|
e1f541d366 | ||
|
|
0f9c468b1e | ||
|
|
ede79f5c57 | ||
|
|
8ddbee199b | ||
|
|
9d63e00632 | ||
|
|
94ec22d466 | ||
|
|
e7709e8756 | ||
|
|
478763c60e | ||
|
|
f0af453020 |
+5
-3
@@ -1,6 +1,6 @@
|
|||||||
# Browser tab
|
# Browser tab
|
||||||
|
|
||||||
One web page, full screen in the tab: no address bar, bookmarks or scrollbars — just the page, in the
|
One web page, full screen in the tab: no address bar, bookmarks, scrollbars or focus highlights — just the page, in the
|
||||||
same area the Graphics tab uses. It loads when the app starts and keeps running while other tabs are
|
same area the Graphics tab uses. It loads when the app starts and keeps running while other tabs are
|
||||||
showing, so it is ready the moment it is opened. Built for It Is Written
|
showing, so it is ready the moment it is opened. Built for It Is Written
|
||||||
(`https://it-is-written-waters-church.vercel.app/`), but any web page works.
|
(`https://it-is-written-waters-church.vercel.app/`), but any web page works.
|
||||||
@@ -10,7 +10,7 @@ showing, so it is ready the moment it is opened. Built for It Is Written
|
|||||||
| Do this | What happens |
|
| Do this | What happens |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Drag a finger | The page scrolls. |
|
| Drag a finger | The page scrolls. |
|
||||||
| Tap a text box on the page | The app's own keyboard (the Prayer tab's) comes up and types into it. Enter submits; the keyboard puts itself away when the page moves on (e.g. after a search). × closes it; tap the box again to bring it back. |
|
| Tap a text box on the page | The app's own keyboard (the Prayer tab's) comes up right under the box (above it if the box is near the bottom of the screen — the stage TV is portrait) and types into it. Enter submits and puts the keyboard away; it also goes away when the page moves on. × closes it; tap the box again to bring it back. |
|
||||||
| Tap a scripture reference (e.g. "Isaiah 41:10") | It opens in a new Bible tab, at that verse, in the translation printed beside it (NIV, ESV…) when that translation is switched on. |
|
| Tap a scripture reference (e.g. "Isaiah 41:10") | It opens in a new Bible tab, at that verse, in the translation printed beside it (NIV, ESV…) when that translation is switched on. |
|
||||||
| Tap anything else | Works on the page as normal. |
|
| Tap anything else | Works on the page as normal. |
|
||||||
| Leave the tab and come back | The page is exactly as it was: same search, same place, same typing. |
|
| Leave the tab and come back | The page is exactly as it was: same search, same place, same typing. |
|
||||||
@@ -65,7 +65,9 @@ the search and could put different results on the program feed.
|
|||||||
- **`browser-guest-preload.cjs`** runs inside the page (invisible to the site) and reports what the app
|
- **`browser-guest-preload.cjs`** runs inside the page (invisible to the site) and reports what the app
|
||||||
cannot see from outside: a tapped text field, a tapped reference (found with the app's own
|
cannot see from outside: a tapped text field, a tapped reference (found with the app's own
|
||||||
`scripture-scan.js` — the same rules ProPresenter Sync uses), scroll, activity, five fingers, and — only
|
`scripture-scan.js` — the same rules ProPresenter Sync uses), scroll, activity, five fingers, and — only
|
||||||
while a follower is linked — a script-free copy of the page's DOM.
|
while a follower is linked — a script-free copy of the page's DOM. It also takes the look (outline,
|
||||||
|
border, glow) out of the site's focus styles: every key on the app's keyboard takes focus from the page
|
||||||
|
for a moment, so a focus highlight would flash with each letter typed.
|
||||||
- **`browser-tab.js`** owns the tab: the pages, the keyboard (via `createPrayerKeyboard` with a "sink"),
|
- **`browser-tab.js`** owns the tab: the pages, the keyboard (via `createPrayerKeyboard` with a "sink"),
|
||||||
scripture taps, scrolling for the phone / OSC / admin, and the follower's copy. On the follower it
|
scripture taps, scrolling for the phone / OSC / admin, and the follower's copy. On the follower it
|
||||||
shows that copy in sandboxed frames (no scripts); in a plain browser (admin offstage copy, `npm start`)
|
shows that copy in sandboxed frames (no scripts); in a plain browser (admin offstage copy, `npm start`)
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ Notes:
|
|||||||
for you. The TV's updater compares versions numerically and only offers a HIGHER version.
|
for you. The TV's updater compares versions numerically and only offers a HIGHER version.
|
||||||
- If `WIN_CSC_LINK` / `CSC_LINK` / `CSC_KEY_PASSWORD` / `CSC_KEYCHAIN` exist in your environment,
|
- If `WIN_CSC_LINK` / `CSC_LINK` / `CSC_KEY_PASSWORD` / `CSC_KEYCHAIN` exist in your environment,
|
||||||
`build-win.cjs` removes them for the build (electron-builder aborts on an EMPTY `WIN_CSC_LINK`).
|
`build-win.cjs` removes them for the build (electron-builder aborts on an EMPTY `WIN_CSC_LINK`).
|
||||||
|
- **Keep `build/installerIcon.ico`.** On macOS electron-builder rebuilds the uninstaller in JavaScript
|
||||||
|
and leaves out NSIS's icon patch; if the installer and uninstaller icons differ, the uninstaller fails
|
||||||
|
NSIS's integrity check, and that version can then never be updated or uninstalled. This file makes
|
||||||
|
both use one icon (NSIS's own default, so nothing looks different). `build-win.cjs` refuses to build
|
||||||
|
without it. Versions 1.4.35–1.4.37 shipped without it; `build/installer.nsh` installs over their
|
||||||
|
damaged uninstallers instead of running them.
|
||||||
- On Windows, if the build fails while extracting `winCodeSign` (symlink / permission errors), run the
|
- On Windows, if the build fails while extracting `winCodeSign` (symlink / permission errors), run the
|
||||||
terminal as administrator or turn on Developer Mode, delete
|
terminal as administrator or turn on Developer Mode, delete
|
||||||
`%LOCALAPPDATA%\electron-builder\Cache\winCodeSign`, and build again.
|
`%LOCALAPPDATA%\electron-builder\Cache\winCodeSign`, and build again.
|
||||||
|
|||||||
@@ -127,6 +127,19 @@ function restoreOsKeyboard(el) {
|
|||||||
if (v === null) el.removeAttribute('inputmode'); else el.setAttribute('inputmode', v);
|
if (v === null) el.removeAttribute('inputmode'); else el.setAttribute('inputmode', v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Where the field is, so the app can open its keyboard right under it: the stage TV is portrait, and a
|
||||||
|
* keyboard parked at the bottom of a 75" screen is a reach to the floor. `field` tells one field from
|
||||||
|
* another, so focus moving to a different one (Tab, the next memorize blank) can move the keyboard too.
|
||||||
|
*/
|
||||||
|
let fieldSeq = 0;
|
||||||
|
const fieldIds = new WeakMap();
|
||||||
|
function fieldInfo(el) {
|
||||||
|
if (!fieldIds.has(el)) fieldIds.set(el, ++fieldSeq);
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
return { field: fieldIds.get(el), rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } };
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Only a TAP raises the keyboard. A page that focuses its own search box as it loads must not throw a
|
* Only a TAP raises the keyboard. A page that focuses its own search box as it loads must not throw a
|
||||||
* keyboard over the stage while nobody is touching it; focus moving between fields (Tab, the next
|
* keyboard over the stage while nobody is touching it; focus moving between fields (Tab, the next
|
||||||
@@ -137,10 +150,13 @@ window.addEventListener('pointerdown', (e) => {
|
|||||||
const el = editableFrom(e.target);
|
const el = editableFrom(e.target);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
quietOsKeyboard(el);
|
quietOsKeyboard(el);
|
||||||
send('bibleapp:editable', { on: true, tap: true });
|
send('bibleapp:editable', Object.assign({ on: true, tap: true }, fieldInfo(el)));
|
||||||
}, true);
|
}, true);
|
||||||
document.addEventListener('focusin', (e) => {
|
document.addEventListener('focusin', (e) => {
|
||||||
send('bibleapp:editable', { on: isEditable(e.target), tap: false });
|
const el = e.target;
|
||||||
|
if (!isEditable(el)) { send('bibleapp:editable', { on: false, tap: false }); return; }
|
||||||
|
// Measured a frame later: focusing can scroll the field into view first.
|
||||||
|
requestAnimationFrame(() => send('bibleapp:editable', Object.assign({ on: true, tap: false }, fieldInfo(el))));
|
||||||
}, true);
|
}, true);
|
||||||
document.addEventListener('focusout', (e) => {
|
document.addEventListener('focusout', (e) => {
|
||||||
restoreOsKeyboard(e.target);
|
restoreOsKeyboard(e.target);
|
||||||
@@ -151,6 +167,71 @@ document.addEventListener('focusout', (e) => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ no focus highlight
|
||||||
|
/*
|
||||||
|
* Nothing on the page shows that it has focus. The app's keyboard is outside the page, so every key press
|
||||||
|
* takes focus from the page for a moment and gives it straight back: a site's focus style would flash
|
||||||
|
* with each letter — and on stage it is clutter anyway. Outline rings (the browser's own and any site's)
|
||||||
|
* go with one rule in browser-tab.js's CLEAN_CSS; this takes the rest — a border that brightens, a glow —
|
||||||
|
* out of the site's own :focus / :focus-within / :focus-visible rules, and leaves everything else those
|
||||||
|
* rules do: a menu that opens on focus still opens. Sites add rules as they go (CSS-in-JS, late
|
||||||
|
* stylesheets), so this runs again just before anything can take focus. Another origin's stylesheets
|
||||||
|
* cannot be read and keep theirs; the outline rule still covers their rings.
|
||||||
|
*/
|
||||||
|
const FOCUS_LOOK = /^(?:outline|box-shadow|border)(?:-|$)/;
|
||||||
|
const seenRules = new WeakSet();
|
||||||
|
|
||||||
|
// Split at commas that are not inside brackets or quotes: "a:hover, b:is(c, d)" -> 2; "x:1; y:f(a;b)" -> 2.
|
||||||
|
function splitTop(text, sep) {
|
||||||
|
const out = [];
|
||||||
|
let depth = 0, quote = '', start = 0;
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const c = text[i];
|
||||||
|
if (quote) { if (c === '\\') i++; else if (c === quote) quote = ''; continue; }
|
||||||
|
if (c === '"' || c === "'") quote = c;
|
||||||
|
else if (c === '(' || c === '[') depth++;
|
||||||
|
else if (c === ')' || c === ']') depth--;
|
||||||
|
else if (c === sep && depth === 0) { out.push(text.slice(start, i).trim()); start = i + 1; }
|
||||||
|
}
|
||||||
|
out.push(text.slice(start).trim());
|
||||||
|
return out.filter(Boolean);
|
||||||
|
}
|
||||||
|
// Styled BECAUSE it has focus. ":not(:focus)" styles a field at rest, so it does not count.
|
||||||
|
function focusDriven(sel) {
|
||||||
|
return /:focus(?:-within|-visible)?(?![\w-])/.test(sel.replace(/:not\((?:[^()]|\([^()]*\))*\)/g, ''));
|
||||||
|
}
|
||||||
|
function isLook(decl) { return FOCUS_LOOK.test(decl.slice(0, decl.indexOf(':')).trim().toLowerCase()); }
|
||||||
|
|
||||||
|
function stripFocusLook(list, owner) {
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
const r = list[i];
|
||||||
|
if (r.cssRules && r.cssRules.length) stripFocusLook(r.cssRules, r); // @media, @supports, @layer, nesting
|
||||||
|
if (!r.selectorText || !r.style || seenRules.has(r)) continue;
|
||||||
|
seenRules.add(r);
|
||||||
|
const sels = splitTop(r.selectorText, ',');
|
||||||
|
const focus = sels.filter(focusDriven);
|
||||||
|
if (!focus.length) continue;
|
||||||
|
// Declarations as written (cssText), so a shorthand built on var() survives intact.
|
||||||
|
const decls = splitTop(r.style.cssText, ';');
|
||||||
|
const keep = decls.filter((d) => !isLook(d));
|
||||||
|
if (keep.length === decls.length) continue;
|
||||||
|
if (focus.length === sels.length) { r.style.cssText = keep.join('; '); continue; }
|
||||||
|
// Shared with selectors that are not about focus ("a:hover, a:focus"): those keep the whole rule, and
|
||||||
|
// the focus ones move to a copy without the look.
|
||||||
|
r.selectorText = sels.filter((s) => !focusDriven(s)).join(', ');
|
||||||
|
if (!keep.length) continue;
|
||||||
|
try { owner.insertRule(focus.join(', ') + ' { ' + keep.join('; ') + ' }', i + 1); i++; seenRules.add(list[i]); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hideFocusLook() {
|
||||||
|
const sheets = [...document.styleSheets, ...(document.adoptedStyleSheets || [])];
|
||||||
|
for (const sh of sheets) { try { stripFocusLook(sh.cssRules, sh); } catch (_) {} } // another origin's: unreadable
|
||||||
|
}
|
||||||
|
window.addEventListener('DOMContentLoaded', hideFocusLook);
|
||||||
|
window.addEventListener('load', hideFocusLook);
|
||||||
|
window.addEventListener('pointerdown', hideFocusLook, true);
|
||||||
|
document.addEventListener('focusin', hideFocusLook, true);
|
||||||
|
|
||||||
// ------------------------------------------------------------------ activity + five fingers
|
// ------------------------------------------------------------------ activity + five fingers
|
||||||
let lastActivity = 0;
|
let lastActivity = 0;
|
||||||
function activity() {
|
function activity() {
|
||||||
|
|||||||
+46
-14
@@ -41,15 +41,18 @@
|
|||||||
var RETRY_MS = 10000;
|
var RETRY_MS = 10000;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The page itself is the only thing on screen: no scrollbars, no blue tap flash, no text selection from
|
* The page itself is the only thing on screen: no scrollbars, no blue tap flash, no focus ring, no text
|
||||||
* a resting finger, and no rubber-band pull that would drag the whole page (or, sideways, swipe back a
|
* selection from a resting finger, and no rubber-band pull that would drag the whole page (or, sideways,
|
||||||
* page). Inputs keep their selection so typing still behaves. Applied to the live page with insertCSS
|
* swipe back a page). Inputs keep their selection so typing still behaves. Applied to the live page with
|
||||||
* and to the booth's copy as a <style>, so both screens look the same.
|
* insertCSS and to the booth's copy as a <style>, so both screens look the same. (The ring matters
|
||||||
|
* twice: every key on the app's keyboard takes focus from the page for a moment, so a ring would flash
|
||||||
|
* with each letter. The site's other focus styles are taken out by browser-guest-preload.cjs.)
|
||||||
*/
|
*/
|
||||||
var CLEAN_CSS =
|
var CLEAN_CSS =
|
||||||
'html,body{scrollbar-width:none!important;overscroll-behavior:none!important;}' +
|
'html,body{scrollbar-width:none!important;overscroll-behavior:none!important;}' +
|
||||||
'::-webkit-scrollbar{display:none!important;width:0!important;height:0!important;}' +
|
'::-webkit-scrollbar{display:none!important;width:0!important;height:0!important;}' +
|
||||||
'*{-webkit-tap-highlight-color:transparent!important;}' +
|
'*{-webkit-tap-highlight-color:transparent!important;}' +
|
||||||
|
':focus,:focus-visible{outline:none!important;}' +
|
||||||
'body *{-webkit-user-select:none!important;user-select:none!important;-webkit-touch-callout:none!important;}' +
|
'body *{-webkit-user-select:none!important;user-select:none!important;-webkit-touch-callout:none!important;}' +
|
||||||
'input,textarea,[contenteditable],[contenteditable] *{-webkit-user-select:text!important;user-select:text!important;}';
|
'input,textarea,[contenteditable],[contenteditable] *{-webkit-user-select:text!important;user-select:text!important;}';
|
||||||
|
|
||||||
@@ -360,7 +363,9 @@
|
|||||||
// Only the page on screen may raise or drop the keyboard; a background page focusing its own
|
// Only the page on screen may raise or drop the keyboard; a background page focusing its own
|
||||||
// search box as it loads must not.
|
// search box as it loads must not.
|
||||||
if (!d || !isShown(v)) return;
|
if (!d || !isShown(v)) return;
|
||||||
if (d.on && d.tap) showKeyboard();
|
if (d.on && d.tap) showKeyboard(v, d);
|
||||||
|
// Focus moved on to a DIFFERENT field while typing (Tab, the next memorize blank): follow it.
|
||||||
|
else if (d.on && kb && kb.el.style.display === 'block' && d.field && d.field !== kb.field) placeKeyboard(v, d);
|
||||||
else if (!d.on) hideKeyboard();
|
else if (!d.on) hideKeyboard();
|
||||||
return;
|
return;
|
||||||
case 'bibleapp:activity':
|
case 'bibleapp:activity':
|
||||||
@@ -600,6 +605,9 @@
|
|||||||
if (name === 'Enter') v.el.sendInputEvent({ type: 'char', keyCode: '\r' });
|
if (name === 'Enter') v.el.sendInputEvent({ type: 'char', keyCode: '\r' });
|
||||||
v.el.sendInputEvent({ type: 'keyUp', keyCode: code });
|
v.el.sendInputEvent({ type: 'keyUp', keyCode: code });
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
// Enter is "done": the search runs and the keyboard gets out of the way of the results. A tap on a
|
||||||
|
// field brings it back.
|
||||||
|
if (name === 'Enter') hideKeyboard();
|
||||||
},
|
},
|
||||||
focus: function () {
|
focus: function () {
|
||||||
var v = activeView();
|
var v = activeView();
|
||||||
@@ -616,20 +624,44 @@
|
|||||||
el.style.display = 'none';
|
el.style.display = 'none';
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
a.createPrayerKeyboard(el, null, 'browser', sink);
|
a.createPrayerKeyboard(el, null, 'browser', sink);
|
||||||
kb = { el: el, placed: false };
|
kb = { el: el, field: null };
|
||||||
return kb;
|
return kb;
|
||||||
}
|
}
|
||||||
function showKeyboard() {
|
/*
|
||||||
|
* The keyboard opens directly under the field being typed into. The stage TV is portrait: anything at
|
||||||
|
* the bottom of the screen is a reach to the floor, and the page's search box is near the top. If the
|
||||||
|
* field is too low for the keyboard to fit under it, it goes just above instead. Re-tapping the same
|
||||||
|
* field while the keyboard is up leaves it wherever it was dragged.
|
||||||
|
*/
|
||||||
|
function placeKeyboard(v, d) {
|
||||||
|
var k = kb;
|
||||||
|
if (!k || !d || !d.rect || !v || !v.el) return;
|
||||||
|
var W = v.el.getBoundingClientRect(), z = v.zoom || 1;
|
||||||
|
var fieldTop = W.top + d.rect.y * z;
|
||||||
|
var fieldBottom = W.top + (d.rect.y + d.rect.h) * z;
|
||||||
|
var fieldMid = W.left + (d.rect.x + d.rect.w / 2) * z;
|
||||||
|
var w = k.el.offsetWidth || 760, h = k.el.offsetHeight || 340, gap = 14;
|
||||||
|
var y = fieldBottom + gap;
|
||||||
|
if (y + h > window.innerHeight - 8) {
|
||||||
|
var above = fieldTop - gap - h;
|
||||||
|
y = above >= 0 ? above : Math.max(0, window.innerHeight - h - 8);
|
||||||
|
}
|
||||||
|
var x = Math.max(0, Math.min(window.innerWidth - w, Math.round(fieldMid - w / 2)));
|
||||||
|
k.el.style.left = x + 'px';
|
||||||
|
k.el.style.top = Math.round(y) + 'px';
|
||||||
|
k.field = d.field || null;
|
||||||
|
}
|
||||||
|
function showKeyboard(v, d) {
|
||||||
if (ROLE !== 'stage') return;
|
if (ROLE !== 'stage') return;
|
||||||
var k = keyboard();
|
var k = keyboard();
|
||||||
if (!k) return;
|
if (!k) return;
|
||||||
k.el.style.display = 'block';
|
var wasHidden = k.el.style.display !== 'block';
|
||||||
// First time: bottom centre, clear of the page's search box at the top. After that, wherever it was dragged.
|
k.el.style.display = 'block'; // shown before measuring: a hidden element has no size
|
||||||
if (!k.placed) {
|
if (d && d.rect && (wasHidden || d.field !== k.field)) placeKeyboard(v, d);
|
||||||
k.placed = true;
|
else if (wasHidden && !k.field) {
|
||||||
var w = k.el.offsetWidth || 760, hgt = k.el.offsetHeight || 340;
|
// No position from the page (should not happen): centre it rather than leave it anywhere odd.
|
||||||
k.el.style.left = Math.max(0, Math.round((window.innerWidth - w) / 2)) + 'px';
|
k.el.style.left = Math.max(0, Math.round((window.innerWidth - (k.el.offsetWidth || 760)) / 2)) + 'px';
|
||||||
k.el.style.top = Math.max(0, Math.round(window.innerHeight - hgt - Math.max(24, window.innerHeight * 0.04))) + 'px';
|
k.el.style.top = Math.max(0, Math.round((window.innerHeight - (k.el.offsetHeight || 340)) / 3)) + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function hideKeyboard() { if (kb) kb.el.style.display = 'none'; }
|
function hideKeyboard() { if (kb) kb.el.style.display = 'none'; }
|
||||||
|
|||||||
@@ -4,3 +4,71 @@
|
|||||||
IfFileExists "$INSTDIR\resources\app.asar.unpacked\vendor\redist\vc_redist.x64.exe" 0 +2
|
IfFileExists "$INSTDIR\resources\app.asar.unpacked\vendor\redist\vc_redist.x64.exe" 0 +2
|
||||||
ExecWait '"$INSTDIR\resources\app.asar.unpacked\vendor\redist\vc_redist.x64.exe" /install /quiet /norestart' $0
|
ExecWait '"$INSTDIR\resources\app.asar.unpacked\vendor\redist\vc_redist.x64.exe" /install /quiet /norestart' $0
|
||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------------------------------
|
||||||
|
; Replaces electron-builder's "is the app running?" check (customCheckAppRunning). Runs at the start of
|
||||||
|
; installing, BEFORE the old version is removed and before any file is copied.
|
||||||
|
;
|
||||||
|
; The stock check only looks for "Bible App.exe". But the app also starts programs from its own folder
|
||||||
|
; - the local AI server (llama-server.exe), speech (whisper), the upscaler - and if the app is closed
|
||||||
|
; hard, those are left running. They hold their files open, so copying the new version fails and the
|
||||||
|
; installer says "Bible App cannot be closed. Please close it manually and click Retry" even though
|
||||||
|
; the app is plainly closed, and Retry can never succeed. So:
|
||||||
|
; 1. ask the app to close (it then stops the AI server itself), and give it a moment;
|
||||||
|
; 2. force-close anything named Bible App.exe that is still there;
|
||||||
|
; 3. force-close the app's own helper programs by name (plain taskkill, nothing to go wrong);
|
||||||
|
; 4. as a backstop, force-close EVERY program running from this install folder, whatever its name.
|
||||||
|
; Step 4 matches by folder, via WMI (works from this 32-bit installer on 64-bit Windows, where a
|
||||||
|
; 32-bit process cannot read a 64-bit process's path). The folder travels in an environment variable
|
||||||
|
; so a quote or space in it can never break the command, and it refuses to act on a bare drive.
|
||||||
|
; No question is asked first: installing this version replaces the app, which is what was asked for.
|
||||||
|
; ---------------------------------------------------------------------------------------------------
|
||||||
|
!macro customCheckAppRunning
|
||||||
|
DetailPrint `Closing "${PRODUCT_NAME}"...`
|
||||||
|
nsExec::Exec `"$SYSDIR\cmd.exe" /c taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "USERNAME eq %USERNAME%"`
|
||||||
|
Pop $0
|
||||||
|
Sleep 2500
|
||||||
|
nsExec::Exec `"$SYSDIR\cmd.exe" /c taskkill /f /t /im "${APP_EXECUTABLE_FILENAME}" /fi "USERNAME eq %USERNAME%"`
|
||||||
|
Pop $0
|
||||||
|
nsExec::Exec `"$SYSDIR\cmd.exe" /c taskkill /f /t /im llama-server.exe /im whisper-cli.exe /im realesrgan-ncnn-vulkan.exe /fi "USERNAME eq %USERNAME%"`
|
||||||
|
Pop $0
|
||||||
|
System::Call 'Kernel32::SetEnvironmentVariable(t "BIBLEAPP_INSTDIR", t "$INSTDIR")'
|
||||||
|
StrCpy $1 "$SYSDIR\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
|
${If} ${FileExists} "$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
|
StrCpy $1 "$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $2 `"$1" -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$$r = $$env:BIBLEAPP_INSTDIR.TrimEnd('\'); if ($$r.Split('\').Count -lt 2) { exit }; $$d = $$r + '\'; Get-CimInstance Win32_Process | Where-Object { $$_.ExecutablePath -and $$_.ExecutablePath.StartsWith($$d, [StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { Stop-Process -Id $$_.ProcessId -Force -ErrorAction SilentlyContinue }"`
|
||||||
|
DetailPrint `Closing anything else still running from "${PRODUCT_NAME}"'s folder...`
|
||||||
|
nsExec::Exec $2
|
||||||
|
Pop $0
|
||||||
|
Sleep 1000
|
||||||
|
!insertmacro bibleAppSkipDamagedUninstaller SHELL_CONTEXT
|
||||||
|
!insertmacro bibleAppSkipDamagedUninstaller HKCU
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------------------------------
|
||||||
|
; Versions 1.4.35 - 1.4.37 were built on macOS before build/installerIcon.ico existed, and their
|
||||||
|
; uninstallers are damaged: they fail NSIS's own integrity check ("Installer integrity check has
|
||||||
|
; failed"). electron-builder rebuilds the uninstaller in JavaScript on macOS and leaves out NSIS's
|
||||||
|
; icon patch, so whenever the installer and uninstaller icons differ (NSIS's two defaults do), the
|
||||||
|
; checksum is wrong. An update runs the PREVIOUS version's uninstaller first, so those versions could
|
||||||
|
; never be updated: the damaged uninstaller fails, and the installer reports "Bible App cannot be
|
||||||
|
; closed" (it also cannot be uninstalled from Windows Settings).
|
||||||
|
;
|
||||||
|
; For exactly those versions, the old uninstaller is not run: its UninstallString is cleared, so
|
||||||
|
; electron-builder's uninstallOldVersion finds nothing to run, and the new files are copied over the
|
||||||
|
; old ones. This installer then writes a fresh, valid uninstaller and registry entry. Every other
|
||||||
|
; version still goes through the normal uninstall-then-install path.
|
||||||
|
; ---------------------------------------------------------------------------------------------------
|
||||||
|
!macro bibleAppSkipDamagedUninstaller ROOT
|
||||||
|
ReadRegStr $3 ${ROOT} "${UNINSTALL_REGISTRY_KEY}" DisplayVersion
|
||||||
|
${If} $3 == "1.4.35"
|
||||||
|
${OrIf} $3 == "1.4.36"
|
||||||
|
${OrIf} $3 == "1.4.37"
|
||||||
|
DetailPrint `Version $3's uninstaller is damaged; installing over it instead.`
|
||||||
|
DeleteRegValue ${ROOT} "${UNINSTALL_REGISTRY_KEY}" UninstallString
|
||||||
|
!ifdef UNINSTALL_REGISTRY_KEY_2
|
||||||
|
DeleteRegValue ${ROOT} "${UNINSTALL_REGISTRY_KEY_2}" UninstallString
|
||||||
|
!endif
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -355,6 +355,9 @@ async function createWindow () {
|
|||||||
quit: () => { try { app.quit(); } catch (_) {} },
|
quit: () => { try { app.quit(); } catch (_) {} },
|
||||||
runInstaller: (installerPath) => {
|
runInstaller: (installerPath) => {
|
||||||
// Launch the installer detached, then quit so it can replace the running app.
|
// Launch the installer detached, then quit so it can replace the running app.
|
||||||
|
// Stop the local AI server FIRST: it runs from the install folder, and one left running holds
|
||||||
|
// its files open, so the installer cannot copy the new version over them ("cannot be closed").
|
||||||
|
try { stopLlamaServer(); } catch (_) {}
|
||||||
try {
|
try {
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const child = spawn(installerPath, [], { detached: true, stdio: 'ignore', windowsHide: false });
|
const child = spawn(installerPath, [], { detached: true, stdio: 'ignore', windowsHide: false });
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bible-app",
|
"name": "bible-app",
|
||||||
"version": "1.4.36",
|
"version": "1.4.41",
|
||||||
"description": "Minimalist Bible App for Portrait-Oriented Touchscreen TV",
|
"description": "Minimalist Bible App for Portrait-Oriented Touchscreen TV",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "main.cjs",
|
"main": "main.cjs",
|
||||||
|
|||||||
+3
-1
@@ -1547,7 +1547,7 @@ body.drawing * {
|
|||||||
}
|
}
|
||||||
/* Removed explicit close button; using hold-to-close gesture on tab */
|
/* Removed explicit close button; using hold-to-close gesture on tab */
|
||||||
|
|
||||||
.bible-icon, .media-icon, .whiteboard-icon, .media-tab-icon, .graphics-tab-icon, .maps-tab-icon, .prayer-tab-icon, .alpha-icon, .notes-icon, .ask-tab-icon {
|
.bible-icon, .media-icon, .whiteboard-icon, .media-tab-icon, .graphics-tab-icon, .maps-tab-icon, .browser-tab-icon, .prayer-tab-icon, .alpha-icon, .notes-icon, .ask-tab-icon {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
@@ -1564,6 +1564,8 @@ body.drawing * {
|
|||||||
.tab.active .graphics-tab-icon,
|
.tab.active .graphics-tab-icon,
|
||||||
.tab:hover .maps-tab-icon,
|
.tab:hover .maps-tab-icon,
|
||||||
.tab.active .maps-tab-icon,
|
.tab.active .maps-tab-icon,
|
||||||
|
.tab:hover .browser-tab-icon,
|
||||||
|
.tab.active .browser-tab-icon,
|
||||||
.tab:hover .prayer-tab-icon,
|
.tab:hover .prayer-tab-icon,
|
||||||
.tab.active .prayer-tab-icon,
|
.tab.active .prayer-tab-icon,
|
||||||
.tab:hover .alpha-icon,
|
.tab:hover .alpha-icon,
|
||||||
|
|||||||
@@ -12,8 +12,14 @@
|
|||||||
* - a clean signing environment. There is no code-signing certificate. electron-builder aborts on
|
* - a clean signing environment. There is no code-signing certificate. electron-builder aborts on
|
||||||
* an EMPTY WIN_CSC_LINK ("Env WIN_CSC_LINK is not correct"), so those variables are removed
|
* an EMPTY WIN_CSC_LINK ("Env WIN_CSC_LINK is not correct"), so those variables are removed
|
||||||
* rather than blanked, and identity auto-discovery is turned off.
|
* rather than blanked, and identity auto-discovery is turned off.
|
||||||
|
* - a working uninstaller. On macOS electron-builder rebuilds the NSIS uninstaller in JavaScript and
|
||||||
|
* skips NSIS's icon patch, so unless the installer and uninstaller use the SAME icon the uninstaller
|
||||||
|
* fails NSIS's integrity check — and then that version can never be updated or uninstalled
|
||||||
|
* (1.4.35-1.4.37 shipped like that). build/installerIcon.ico makes electron-builder use one icon for
|
||||||
|
* both, so this refuses to build without it.
|
||||||
*/
|
*/
|
||||||
const { spawnSync } = require('child_process');
|
const { spawnSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const env = { ...process.env, CSC_IDENTITY_AUTO_DISCOVERY: 'false' };
|
const env = { ...process.env, CSC_IDENTITY_AUTO_DISCOVERY: 'false' };
|
||||||
@@ -21,6 +27,13 @@ for (const key of ['CSC_LINK', 'WIN_CSC_LINK', 'CSC_KEY_PASSWORD', 'WIN_CSC_KEY_
|
|||||||
delete env[key];
|
delete env[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ICON = path.join(__dirname, '..', 'build', 'installerIcon.ico');
|
||||||
|
if (!fs.existsSync(ICON)) {
|
||||||
|
console.error('build/installerIcon.ico is missing. Without it the uninstaller built on macOS is damaged');
|
||||||
|
console.error('("Installer integrity check has failed") and the version could never be updated. See tools/build-win.cjs.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const extra = process.argv.slice(2);
|
const extra = process.argv.slice(2);
|
||||||
const args = ['electron-builder', '--win', '--x64', ...extra];
|
const args = ['electron-builder', '--win', '--x64', ...extra];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user