first run, updates, installer: a setup tour stands in front of a fresh install, releases arrive through an in-app updater with stable and nightly channels behind a read-only token, and the branded installer builds again

This commit is contained in:
KeelrLeek committed 2026-09-09 16:16:19 -05:00
1 parent 07810f7543
commit cd2391198e
19 files changed
+2010 -15

No files matched your search

+7
View File
@@ -226,6 +226,13 @@ export const refreshAlbumArt = (albumId) => invoke("refresh_album_art", { albumI
export const refreshAllAlbumArt = () => invoke("refresh_all_album_art");
export const getSettings = () => invoke("get_settings");
export const setSettings = (patch) => invoke("set_settings", patch);
// The in-app updater (updater.rs): status, a check on the current channel,
// settings (channel / autoCheck / endpointStable / endpointNightly), and the
// install — which on Windows ends this process when it succeeds.
export const updaterStatus = () => invoke("updater_status");
export const updaterCheck = () => invoke("updater_check");
export const updaterSet = (patch) => invoke("updater_set", patch);
export const updaterInstall = () => invoke("updater_install");
export const windowGlass = (on) => invoke("window_glass", { on });
export const forgetCredentials = () => invoke("forget_credentials");
export const pickFolder = () => invoke("pick_folder");
+29
View File
@@ -0,0 +1,29 @@
// The first-run setup's one piece of shared state: whether the tour stands
// in front of the app. App.svelte opens it when the settings arrive without
// `setup_done`; Settings → Account opens it again on request; the tour
// itself closes it and stamps the flag. The flag lives in config.json (per
// install, so it travels with a portable data dir), not in the webview.
import { setSettings } from "./api.js";
export const firstRun = $state({
open: false,
/** "boot" = a fresh install, "settings" = run again by hand. */
reason: "boot",
});
export function openFirstRun(reason = "settings") {
firstRun.reason = reason;
firstRun.open = true;
}
/** Close the tour and remember that it ran. The flag write is best effort:
* a failed save re-opens the tour on the next launch, which is the honest
* outcome — nothing else in the app depends on it. */
export async function finishFirstRun() {
firstRun.open = false;
try {
await setSettings({ setupDone: true });
} catch {
// see above
}
}
+4
View File
@@ -18,6 +18,7 @@ import {
notePlaybackGeneration,
} from "./preview/previewSession.svelte.js";
import { CROSSFADE_MS as PREVIEW_CROSSFADE_MS } from "./preview/service.js";
import { firstRun, openFirstRun, finishFirstRun } from "./firstRun.svelte.js";
export const app = $state({
connection: "offline", // offline | connecting | online
@@ -7836,5 +7837,8 @@ if (import.meta.env.DEV) {
followNewsToast, announceFollowNews,
searchTabs, selectSearchTab, closeSearchTab, filesForGroup,
downloads, network, sendDm, openChat,
// The first-run tour: probes on a fresh data dir need to open it,
// drive it, or clear it out of the way.
firstRun, openFirstRun, finishFirstRun,
};
}
+79
View File
@@ -0,0 +1,79 @@
// The updater's view of things: the backend's status snapshot (updater.rs),
// download progress, and whether the toast was waved away. App.svelte
// starts the watch once; Settings → Updates and UpdateToast read it.
import * as api from "./api.js";
export const updater = $state({
/** updater.rs `Status`, or null before the first read. */
status: null,
/** { received, total } while an install downloads. */
progress: null,
/** The toast for the current find was dismissed. */
dismissed: false,
error: "",
});
export const updateAvailable = () => updater.status?.available ?? null;
export async function refreshUpdater() {
try {
updater.status = await api.updaterStatus();
} catch (e) {
updater.error = String(e);
}
}
export async function checkForUpdates() {
updater.error = "";
updater.dismissed = false;
try {
updater.status = await api.updaterCheck();
} catch (e) {
// The status carries last_error too; this is the same text, live.
updater.error = String(e);
await refreshUpdater();
}
}
export async function setUpdater(patch) {
updater.error = "";
try {
updater.status = await api.updaterSet(patch);
updater.dismissed = false;
} catch (e) {
updater.error = String(e);
}
}
/** On success the app exits into the installer; on failure the error
* lands here and in the status. */
export async function installUpdate() {
updater.error = "";
try {
await api.updaterInstall();
} catch (e) {
updater.error = String(e);
await refreshUpdater();
}
}
export function dismissUpdate() {
updater.dismissed = true;
}
let started = false;
export function startUpdaterWatch() {
if (started) return;
started = true;
api.onEvent("updater:status", (s) => {
const before = updater.status?.available?.version ?? null;
updater.status = s;
// A new find un-dismisses the toast; the same one stays dismissed.
if ((s?.available?.version ?? null) !== before) updater.dismissed = false;
if (!s?.installing) updater.progress = null;
});
api.onEvent("updater:progress", (p) => {
updater.progress = p;
});
void refreshUpdater();
}