Wires electrobun's Updater to the /dl download host. The packaged app checks https://miniscp.com/dl on launch + every 6h; when a newer build exists, an accent-colored "update tool" appears next to Connections in the tab bar. It announces the update, becomes its own inline progress bar while downloading, then flips to "Restart to apply" (two-step: download, then user-triggered relaunch). - electrobun.config.ts: release.baseUrl = https://miniscp.com/dl (baked into version.json at build; miniscp.com fronted by an external proxy for TLS). - rpc.ts: checkForUpdate/downloadUpdate/applyUpdate requests + updateStatus push. - rpc-handlers.ts + index.ts: call Updater; poll on launch+6h; stream onStatusChange progress. checkForUpdate self-guards the dev channel. - rpc.service.ts + new updater.service.ts: signal-based state (available -> downloading -> ready, ready derived from downloadUpdate ok). - tab-bar.ts: single bg-primary pill; progress fills the pill itself. - icons.ts: register mynaDownload. i18n: tabBar + menu keys across all locales. - menu.ts + app.ts: "Check for Updates…" app-menu item -> UpdaterService.check(). - docs: build-and-release + architecture updated. Unsigned builds still update (applyUpdate strips the macOS quarantine xattr). Verified: tsc --noEmit (exit 0) + build:ui AOT (exit 0). Live pill + full download/apply flow require a two-build test on the host. Co-Authored-By: Claude <noreply@anthropic.com>
58 lines
1.7 KiB
TypeScript
58 lines
1.7 KiB
TypeScript
import { Injectable, inject, signal } from '@angular/core';
|
|
import { RpcService } from './rpc.service';
|
|
|
|
export type UpdaterState = 'idle' | 'available' | 'downloading' | 'ready' | 'error';
|
|
|
|
/**
|
|
* Drives the tab-bar update pill. The backend pushes updateStatus events
|
|
* (available / downloading progress / error); "ready" is derived locally when
|
|
* downloadUpdate resolves ok. Two-step UX: available -> download -> ready ->
|
|
* apply (relaunch).
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class UpdaterService {
|
|
private rpc = inject(RpcService);
|
|
|
|
readonly state = signal<UpdaterState>('idle');
|
|
readonly version = signal('');
|
|
readonly progress = signal(0);
|
|
|
|
constructor() {
|
|
if (!this.rpc.available) return;
|
|
this.rpc.onUpdateStatus((s) => {
|
|
if (s.phase === 'available') {
|
|
this.version.set(s.version ?? '');
|
|
this.state.set('available');
|
|
} else if (s.phase === 'downloading') {
|
|
this.state.set('downloading');
|
|
this.progress.set(s.progress ?? 0);
|
|
} else if (s.phase === 'error') {
|
|
this.state.set('error');
|
|
}
|
|
});
|
|
}
|
|
|
|
async download(): Promise<void> {
|
|
if (this.state() !== 'available') return;
|
|
this.state.set('downloading');
|
|
this.progress.set(0);
|
|
const r = await this.rpc.request.downloadUpdate({});
|
|
this.state.set(r?.ok ? 'ready' : 'error');
|
|
}
|
|
|
|
apply(): void {
|
|
if (this.state() !== 'ready') return;
|
|
void this.rpc.request.applyUpdate({}); // quits + relaunches
|
|
}
|
|
|
|
/** Manual re-check (menu item). */
|
|
async check(): Promise<void> {
|
|
if (!this.rpc.available) return;
|
|
const r = await this.rpc.request.checkForUpdate({});
|
|
if (r?.ok && r.updateAvailable) {
|
|
this.version.set(r.version);
|
|
this.state.set('available');
|
|
}
|
|
}
|
|
}
|