setup: the installer is our own window — the alpha key is the gate, the release comes down with it and is verified before the silent install, and the in-app tour asks for the key too
This commit is contained in:
1 parent
8093835482
commit
f8508da081
17 files changed
+1960
-13
No files matched your search
@@ -27,6 +27,7 @@
|
||||
|
||||
const STEPS = [
|
||||
{ id: "welcome", label: "Welcome", icon: "sparkle" },
|
||||
{ id: "alpha", label: "Alpha key", icon: "key" },
|
||||
{ id: "library", label: "Your library", icon: "library" },
|
||||
{ id: "soulseek", label: "Soulseek", icon: "users" },
|
||||
{ id: "community", label: "Community", icon: "globe" },
|
||||
@@ -53,6 +54,10 @@
|
||||
let displayName = $state("");
|
||||
let glass = $state(false);
|
||||
let loudness = $state(true);
|
||||
// The closed alpha's read-only release key (updater.rs): typed here,
|
||||
// kept by the backend in the Credential Manager, never read back.
|
||||
let alphaKey = $state("");
|
||||
let hasToken = $state(false);
|
||||
let busy = $state(false);
|
||||
let error = $state("");
|
||||
let closing = $state(false);
|
||||
@@ -90,6 +95,11 @@
|
||||
} catch {
|
||||
freenetOn = false;
|
||||
}
|
||||
try {
|
||||
hasToken = !!(await api.updaterStatus()).has_token;
|
||||
} catch {
|
||||
hasToken = false;
|
||||
}
|
||||
await tick();
|
||||
panelEl?.querySelector(".primary")?.focus();
|
||||
}
|
||||
@@ -132,7 +142,25 @@
|
||||
|
||||
async function next() {
|
||||
if (busy || closing) return;
|
||||
if (cur.id === "library") {
|
||||
if (cur.id === "alpha") {
|
||||
const k = alphaKey.trim();
|
||||
if (k) {
|
||||
busy = true;
|
||||
error = "";
|
||||
try {
|
||||
await api.updaterSet({ token: k });
|
||||
hasToken = true;
|
||||
alphaKey = "";
|
||||
// The first check can start now; the toast reports it.
|
||||
void api.updaterCheck().catch(() => {});
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
if (error) return;
|
||||
}
|
||||
} else if (cur.id === "library") {
|
||||
const patch = {};
|
||||
if (libraryDir && libraryDir !== settings?.download_dir) patch.downloadDir = libraryDir;
|
||||
const lay = layout.trim();
|
||||
@@ -274,11 +302,28 @@
|
||||
that runs on a peer you own. Four short questions and the app is yours.
|
||||
</p>
|
||||
<ul class="pills" in:fly={{ y: 10, duration: dur(520), delay: dur(460), easing: quintOut }}>
|
||||
{#each STEPS.slice(1, 5) as s (s.id)}
|
||||
{#each STEPS.slice(1, 6) as s (s.id)}
|
||||
<li><Icon name={s.icon} size={13} /> {s.label}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{:else if cur.id === "alpha"}
|
||||
<h1 id="fr-title">Your alpha key</h1>
|
||||
<p class="lede">
|
||||
SoulStream is in a closed alpha. New builds come from a private repository, and
|
||||
this key — one per tester, able only to read them — is how your copy finds
|
||||
updates. It goes to the Windows Credential Manager, not a plain file. Without it
|
||||
the app runs, but cannot update itself.
|
||||
</p>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="label">
|
||||
<div class="name">Alpha key {#if hasToken}<span class="tag ok">set</span>{/if}</div>
|
||||
<div class="hint">{hasToken ? "A key is saved on this computer. Paste another to replace it, or press Next to keep it." : "Paste the key you were given. It starts with github_pat_."}</div>
|
||||
</div>
|
||||
<div class="ctl"><input type="password" class="mono" bind:value={alphaKey} placeholder={hasToken ? "••••••••••••" : "github_pat_…"} spellcheck="false" autocomplete="off" aria-label="Alpha key" /></div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if cur.id === "library"}
|
||||
<h1 id="fr-title">Where does your music live?</h1>
|
||||
<p class="lede">
|
||||
@@ -400,6 +445,7 @@
|
||||
<p class="lede">Here is what you chose. Every line of it lives in Settings.</p>
|
||||
<ul class="summary">
|
||||
{#each [
|
||||
["key", "Alpha key", hasToken ? "set" : "not set — updates need it"],
|
||||
["folder-open", "Library", libraryDir || "not chosen"],
|
||||
["users", "Soulseek", online ? `signed in as ${app.username}` : "not signed in yet"],
|
||||
["globe", "Community peer", freenetOn ? (displayName.trim() ? `on · ${displayName.trim()}` : "on") : "off"],
|
||||
@@ -430,7 +476,7 @@
|
||||
{:else if cur.id === "ready"}
|
||||
<button class="ax-fill primary big" onclick={finish} disabled={closing}>Open SoulStream</button>
|
||||
{:else}
|
||||
{#if cur.id === "soulseek" && !online}
|
||||
{#if (cur.id === "soulseek" && !online) || (cur.id === "alpha" && !hasToken && !alphaKey.trim())}
|
||||
<button class="lnk" onclick={next} disabled={busy}>Later</button>
|
||||
{/if}
|
||||
<button class="ax-fill primary" onclick={next} disabled={busy || closing}>{busy ? "Saving…" : "Next"} <Icon name="chevron-right" size={14} /></button>
|
||||
|
||||
@@ -3,17 +3,34 @@
|
||||
// the other toasts, but it STAYS until answered — Install here, or Later
|
||||
// (the Settings card keeps the offer). Installing on its own was never on
|
||||
// the table; this is the button the updater's note talks about.
|
||||
//
|
||||
// Its other face, for the closed alpha: a copy whose check failed for
|
||||
// want of the read-only key says so once per session, with the way to
|
||||
// Settings — a tester who skipped the tour's key step still finds out.
|
||||
import Icon from "./Icon.svelte";
|
||||
import { updater, installUpdate, dismissUpdate } from "../lib/updater.svelte.js";
|
||||
import { goToView } from "../lib/state.svelte.js";
|
||||
import { updater, installUpdate, dismissUpdate, dismissNudge } from "../lib/updater.svelte.js";
|
||||
|
||||
const available = $derived(updater.status?.available ?? null);
|
||||
const installing = $derived(!!updater.status?.installing);
|
||||
const shown = $derived(!!available && !updater.dismissed);
|
||||
const needsKey = $derived(
|
||||
!available
|
||||
&& !!updater.status
|
||||
&& !updater.status.has_token
|
||||
&& /access token/.test(updater.status.last_error ?? "")
|
||||
&& !updater.nudgeDismissed,
|
||||
);
|
||||
const pct = $derived.by(() => {
|
||||
const p = updater.progress;
|
||||
if (!p || !p.total) return null;
|
||||
return Math.min(100, Math.round((p.received / p.total) * 100));
|
||||
});
|
||||
|
||||
function openSettings() {
|
||||
dismissNudge();
|
||||
goToView("settings");
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if shown}
|
||||
@@ -30,6 +47,15 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if needsKey}
|
||||
<div class="layer" aria-live="polite">
|
||||
<div class="toast" role="status">
|
||||
<span class="ic"><Icon name="key" size={14} /></span>
|
||||
<span class="text">Updates need your <b>alpha key</b></span>
|
||||
<button class="go" onclick={openSettings}>Open Settings</button>
|
||||
<button class="later" onclick={dismissNudge} title="Not now" aria-label="Not now"><Icon name="x" size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -10,9 +10,15 @@ export const updater = $state({
|
||||
progress: null,
|
||||
/** The toast for the current find was dismissed. */
|
||||
dismissed: false,
|
||||
/** The "updates need your alpha key" nudge was waved away this session. */
|
||||
nudgeDismissed: false,
|
||||
error: "",
|
||||
});
|
||||
|
||||
export function dismissNudge() {
|
||||
updater.nudgeDismissed = true;
|
||||
}
|
||||
|
||||
export const updateAvailable = () => updater.status?.available ?? null;
|
||||
|
||||
export async function refreshUpdater() {
|
||||
|
||||
Reference in new issue
Block a user