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:
1 parent
07810f7543
commit
cd2391198e
19 files changed
+2010
-15
No files matched your search
@@ -8,6 +8,21 @@
|
||||
import { app, network, clearCatalogHeroArtCache, refreshSourcePrefs } from "../lib/state.svelte.js";
|
||||
import { setWordFill } from "../lib/lyrics.svelte.js";
|
||||
import { applyGlass } from "../lib/glass.js";
|
||||
import { openFirstRun } from "../lib/firstRun.svelte.js";
|
||||
import { updater, checkForUpdates, setUpdater, installUpdate } from "../lib/updater.svelte.js";
|
||||
// The updater card reads the backend's snapshot; `pct` is the install download.
|
||||
const up = $derived(updater.status);
|
||||
const avail = $derived(up?.available ?? null);
|
||||
const pct = $derived(updater.progress?.total ? Math.min(100, Math.round((updater.progress.received / updater.progress.total) * 100)) : null);
|
||||
// The private-release token: typed here, saved to the Credential Manager
|
||||
// by the backend, never read back into the page.
|
||||
let tokenDraft = $state("");
|
||||
async function saveToken() {
|
||||
const t = tokenDraft.trim();
|
||||
if (!t) return;
|
||||
await setUpdater({ token: t });
|
||||
tokenDraft = "";
|
||||
}
|
||||
import * as api from "../lib/api.js";
|
||||
import Icon from "../components/Icon.svelte";
|
||||
import Chooser from "../components/Chooser.svelte";
|
||||
@@ -329,6 +344,7 @@
|
||||
const SECTIONS = [
|
||||
{ id: "account", title: "Account", group: "You", icon: "users" },
|
||||
{ id: "networks", title: "Networks", group: "You", icon: "globe" },
|
||||
{ id: "updates", title: "Updates", group: "You", icon: "download" },
|
||||
{ id: "appearance", title: "Appearance", group: "Look & sound", icon: "eye" },
|
||||
{ id: "playback", title: "Playback", group: "Look & sound", icon: "volume" },
|
||||
{ id: "lyrics", title: "Lyrics", group: "Look & sound", icon: "lyrics" },
|
||||
@@ -423,6 +439,13 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">
|
||||
<div class="name">First-time setup</div>
|
||||
<div class="hint">The welcome tour from the first launch: library folder, account, community peer, look. Runs again from here and changes nothing until you do.</div>
|
||||
</div>
|
||||
<div class="ctl"><button class="ax-ghost" onclick={() => openFirstRun("settings")}>Run again</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -525,6 +548,72 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Updates ─────────────────────────────────────────── -->
|
||||
<section id="set-updates" class="sec">
|
||||
<div class="sec-head"><span class="hic"><Icon name={iconOf("updates")} size={14} /></span><h2>Updates</h2><span class="blurb">New releases from the project, on the channel you pick. Installing is always a button.</span></div>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="label">
|
||||
<div class="name">SoulStream {up?.version ?? "…"} {#if up?.dev_build}<span class="tag off">dev build</span>{/if}{#if avail}<span class="tag acc">{avail.version} available</span>{:else if up?.last_check && !up?.last_error}<span class="tag ok">up to date</span>{/if}</div>
|
||||
<div class="hint">
|
||||
{#if up?.checking}Checking {up.channel}…
|
||||
{:else if up?.last_error}<span class="warn">Last check failed: {up.last_error}</span>
|
||||
{:else if up?.last_check}Checked {fmtWhen(up.last_check * 1000)} on the {up.channel} channel.
|
||||
{:else}Not checked yet on the {up?.channel ?? "stable"} channel.{/if}
|
||||
{#if updater.error}<span class="warn"> {updater.error}</span>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctl"><button class="ax-ghost" onclick={() => checkForUpdates()} disabled={!!up?.checking || !!up?.installing}>{up?.checking ? "Checking…" : "Check now"}</button></div>
|
||||
</div>
|
||||
{#if avail}
|
||||
<div class="row">
|
||||
<div class="label">
|
||||
<div class="name">{avail.version} is ready {#if avail.date}<span class="tag">{new Date(avail.date * 1000).toLocaleDateString()}</span>{/if}</div>
|
||||
{#if avail.notes}<div class="hint notes">{avail.notes}</div>{/if}
|
||||
{#if up?.installing}
|
||||
<div class="hint">{pct != null ? `Downloading · ${pct}%` : "Downloading…"} — the app closes into the installer and comes back on its own.</div>
|
||||
<span class="pbar" aria-hidden="true"><i style="width: {pct ?? 6}%"></i></span>
|
||||
{:else if up?.dev_build}
|
||||
<div class="hint warn">This is a dev build run from the target folder; the installer would set up the packaged app beside it.</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="ctl"><button class="ax-fill" onclick={() => installUpdate()} disabled={!!up?.installing}><Icon name="download" size={12} /> {up?.installing ? "Installing…" : "Install and restart"}</button></div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="row">
|
||||
<div class="label">
|
||||
<div class="name">Channel</div>
|
||||
<div class="hint">Stable follows the releases. Nightly is built from <b>main</b> every day and can break; switching back to Stable offers the next stable build even when its number is lower.</div>
|
||||
</div>
|
||||
<div class="ctl">{@render seg([["stable", "Stable"], ["nightly", "Nightly"]], up?.channel ?? "stable", (v) => setUpdater({ channel: v }), !up || !!up.installing)}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">
|
||||
<div class="name">Check automatically</div>
|
||||
<div class="hint">Shortly after launch and every six hours. Finding one shows a toast; nothing installs on its own.</div>
|
||||
</div>
|
||||
<div class="ctl">{@render sw(up?.auto_check ?? true, ["on", "off"], (v) => setUpdater({ autoCheck: v }), !up)}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">
|
||||
<div class="name">Access token {#if up?.has_token}<span class="tag ok">set</span>{:else if up?.source_kind === "github"}<span class="tag warn">needed for a private repository</span>{/if}</div>
|
||||
<div class="hint">The project's repository is private while the alpha runs: releases are read with a token that can only read them. Paste the one you were given — it goes to the Windows Credential Manager, not a plain file, and leaves with Clear.</div>
|
||||
</div>
|
||||
<div class="ctl">
|
||||
<input type="password" class="mono tok" bind:value={tokenDraft} placeholder={up?.has_token ? "••••••••••••" : "github_pat_…"} spellcheck="false" autocomplete="off" aria-label="Access token" onkeydown={(e) => e.key === "Enter" && saveToken()} disabled={!up} />
|
||||
<button class="ax-white" onclick={saveToken} disabled={!tokenDraft.trim()}>Save</button>
|
||||
{#if up?.has_token}<button class="ax-ghost" onclick={() => setUpdater({ token: "" })}>Clear</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<details class="disclose src">
|
||||
<summary>Update source</summary>
|
||||
<div class="hint">The manifest each channel reads (<code>latest.json</code>, Tauri's updater format, signed with the project's key). Leave empty for the project's GitHub releases; point it at a mirror or your own Gitea to update from there.</div>
|
||||
<label class="srcrow"><span>Stable</span><input class="mono" value={up?.endpoint_stable ?? ""} placeholder={up?.default_stable ?? ""} spellcheck="false" onkeydown={(e) => e.key === "Enter" && e.currentTarget.blur()} onchange={(e) => setUpdater({ endpointStable: e.currentTarget.value })} /></label>
|
||||
<label class="srcrow"><span>Nightly</span><input class="mono" value={up?.endpoint_nightly ?? ""} placeholder={up?.default_nightly ?? ""} spellcheck="false" onkeydown={(e) => e.key === "Enter" && e.currentTarget.blur()} onchange={(e) => setUpdater({ endpointNightly: e.currentTarget.value })} /></label>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Appearance ──────────────────────────────────────── -->
|
||||
<section id="set-appearance" class="sec">
|
||||
<div class="sec-head"><span class="hic"><Icon name={iconOf("appearance")} size={14} /></span><h2>Appearance</h2><span class="blurb">How the window is drawn.</span></div>
|
||||
@@ -840,6 +929,15 @@
|
||||
.subgrp { display: flex; align-items: center; gap: 8px; padding: 12px 16px 2px; font-size: 10.5px; font-weight: 900; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); }
|
||||
.subgrp .ln { flex: 1; height: 1px; background: var(--hair); }
|
||||
.noderow { display: flex; align-items: center; gap: 10px; margin-top: 8px; flex-wrap: wrap; }
|
||||
.hint.notes { white-space: pre-wrap; max-height: 9em; overflow: auto; }
|
||||
.pbar { display: block; width: min(100%, 360px); height: 4px; border-radius: 99px; background: var(--surf-hi); overflow: hidden; margin-top: 8px; }
|
||||
.pbar i { display: block; height: 100%; background: var(--accent); border-radius: 99px; transition: width 0.2s; }
|
||||
.disclose.src { padding: 10px 16px 14px; border-top: 1px solid var(--hair); }
|
||||
.disclose.src .hint { margin: 6px 0 10px; }
|
||||
.disclose.src code { font-family: "Cascadia Code", Consolas, monospace; font-size: 11px; }
|
||||
.srcrow { display: grid; grid-template-columns: 64px 1fr; align-items: center; gap: 10px; margin-top: 6px; font-size: 12px; font-weight: 700; color: var(--text-secondary); }
|
||||
.srcrow input { width: 100%; min-width: 0; }
|
||||
input.tok { width: 220px; }
|
||||
.lnk { color: var(--text-secondary); font-weight: 700; text-decoration: underline; text-underline-offset: 2px; font-size: 12px; }
|
||||
.lnk:disabled { opacity: 0.5; }
|
||||
.disclose { margin-top: 8px; }
|
||||
|
||||
Reference in new issue
Block a user