Build 115. Downloading and then telling the operator to quit, mount and drag was the friction the updater was supposed to remove. An app cannot replace itself while running, so the swap is done by a small script that outlives the process: it waits for us to exit, moves the old bundle aside, copies the new one in, clears the quarantine flag and relaunches. Moved aside, not deleted. If the copy fails half way — a full disk, a truncated download, the volume going away — the old app goes straight back and is reopened. The failure this is written against is a campus Mac with no working app and a service in an hour, which is far worse than not updating. Tested in a sandbox across all three paths: a clean swap, a copy that fails mid-way (old app restored, no leftovers), and a disk image with no app in it (aborts before touching the installed one). It refuses while translation is live: quitting stops every listener's captions and no update is worth that. Stop translating first. No sudo — /Applications is admin-writable and the bundle belongs to the operator. If that is ever untrue the copy fails and the rollback runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
3.5 KiB
JavaScript
63 lines
3.5 KiB
JavaScript
/*
|
|
* preload.js — the only bridge between the renderer and Node.
|
|
* Nothing here takes a path or a command from the page; every call is a fixed
|
|
* verb with no free-form arguments beyond a config patch.
|
|
*/
|
|
'use strict';
|
|
|
|
const { contextBridge, ipcRenderer } = require('electron');
|
|
|
|
contextBridge.exposeInMainWorld('engineHost', {
|
|
getConfig: () => ipcRenderer.invoke('config:get'),
|
|
setConfig: (patch) => ipcRenderer.invoke('config:set', patch),
|
|
micStatus: () => ipcRenderer.invoke('mic:status'),
|
|
askMic: () => ipcRenderer.invoke('mic:ask'),
|
|
openPrivacy: () => ipcRenderer.invoke('app:openPrivacy'),
|
|
cacheInfo: () => ipcRenderer.invoke('app:cacheInfo'),
|
|
revealCache: () => ipcRenderer.invoke('app:revealCache'),
|
|
copyText: (text) => ipcRenderer.invoke('app:copy', text),
|
|
setLive: (live) => ipcRenderer.send('engine:live', !!live),
|
|
logLine: (line) => ipcRenderer.send('log:line', String(line || '')),
|
|
|
|
/* Translation lives in the main process; these are its only two verbs. */
|
|
mtEnsure: () => ipcRenderer.invoke('mt:ensure'),
|
|
updateCheck: () => ipcRenderer.invoke('update:check'),
|
|
updateDownload: (u, n) => ipcRenderer.invoke('update:download', u, n),
|
|
updateReveal: (p) => ipcRenderer.invoke('update:reveal', p),
|
|
updateInstall: (p) => ipcRenderer.invoke('update:install', p),
|
|
onUpdateProgress: (fn) => ipcRenderer.on('update:progress', (_e, p) => fn(p)),
|
|
settingsExport: () => ipcRenderer.invoke('settings:export'),
|
|
settingsImport: () => ipcRenderer.invoke('settings:import'),
|
|
audioDevices: () => ipcRenderer.invoke('audio:devices'),
|
|
modelsCached: (id) => ipcRenderer.invoke('models:cached', id),
|
|
mtTranslate: (req) => ipcRenderer.invoke('mt:translate', req),
|
|
mtTranslateMulti: (req) => ipcRenderer.invoke('mt:translateMulti', req),
|
|
onMtProgress: (fn) => ipcRenderer.on('mt:progress', (_e, p) => fn(p)),
|
|
onMtDown: (fn) => ipcRenderer.on('mt:down', (_e, info) => fn(info)),
|
|
|
|
/* The congregation server runs inside this app; these describe it. */
|
|
localInfo: () => ipcRenderer.invoke('local:info'),
|
|
onServerUp: (fn) => ipcRenderer.on('server:up', (_e, info) => fn(info)),
|
|
onServerDown: (fn) => ipcRenderer.on('server:down', () => fn()),
|
|
|
|
/* Share to the internet: a Cloudflare quick tunnel for livestream viewers. */
|
|
tunnelStart: () => ipcRenderer.invoke('tunnel:start'),
|
|
tunnelStop: () => ipcRenderer.invoke('tunnel:stop'),
|
|
tunnelStatus: () => ipcRenderer.invoke('tunnel:status'),
|
|
onTunnel: (fn) => ipcRenderer.on('tunnel:status', (_e, s) => fn(s)),
|
|
|
|
/* Built-in voices: this Mac speaks, and the phones play what it sends. */
|
|
ttsInfo: () => ipcRenderer.invoke('tts:info'),
|
|
ttsSpeak: (req) => ipcRenderer.invoke('tts:speak', req),
|
|
ttsPreload: (langs) => ipcRenderer.invoke('tts:preload', langs),
|
|
/* Audition one specific voice, and save the per-language choices. */
|
|
ttsAudition: (req) => ipcRenderer.invoke('tts:audition', req),
|
|
ttsSetVoices: (c) => ipcRenderer.invoke('tts:setVoices', c),
|
|
ttsSetListenerVoices: (m) => ipcRenderer.invoke('tts:setListenerVoices', m),
|
|
onTtsReady: (fn) => ipcRenderer.on('tts:ready', (_e, i) => fn(i)),
|
|
onTtsProgress: (fn) => ipcRenderer.on('tts:progress', (_e, p) => fn(p)),
|
|
onTtsCloud: (fn) => ipcRenderer.on('tts:cloud', (_e, s) => fn(s)),
|
|
onLocalVoices: (fn) => ipcRenderer.on('localvoices:status', (_e, s) => fn(s)),
|
|
localVoicesStatus: () => ipcRenderer.invoke('localvoices:status')
|
|
});
|