refactor Start.svelte

This commit is contained in:
llama 2025-10-18 10:08:29 +08:00
parent 3bb77f964d
commit 49e850c7da
No known key found for this signature in database
GPG key ID: 0B7543854B9413C3

View file

@ -3,32 +3,32 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import * as tr from "@generated/ftl-launcher";
import {
Mirror,
type Options,
type Options as OptionsProto,
type ChooseVersionResponse,
type GetLangsResponse_Pair,
type GetMirrorsResponse_Pair,
} from "@generated/anki/launcher_pb";
import { chooseVersion } from "@generated/backend-launcher";
import {
chooseVersion,
getAvailableVersions,
getExistingVersions,
} from "@generated/backend-launcher";
import Row from "$lib/components/Row.svelte";
import EnumSelectorRow from "$lib/components/EnumSelectorRow.svelte";
import SettingTitle from "$lib/components/SettingTitle.svelte";
import TitledContainer from "$lib/components/TitledContainer.svelte";
import Container from "$lib/components/Container.svelte";
import SwitchRow from "$lib/components/SwitchRow.svelte";
import EnumSelector from "$lib/components/EnumSelector.svelte";
import { tr } from "./stores";
import Warning from "./Warning.svelte";
import { listen } from "@tauri-apps/api/event";
import { protoBase64 } from "@bufbuild/protobuf";
import { currentLang, versionsStore } from "./stores";
import { onMount } from "svelte";
import Action from "./Action.svelte";
import Spinner from "./Spinner.svelte";
import Options from "./Options.svelte";
import Term from "./Term.svelte";
import AnkiWillStart from "./AnkiWillStart.svelte";
import { Terminal } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
// TODO: why
/* eslint-disable prefer-const */
let {
langs,
selectedLang = $bindable(),
@ -37,241 +37,119 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}: {
langs: GetLangsResponse_Pair[];
selectedLang: string;
options: Options;
options: OptionsProto;
mirrors: GetMirrorsResponse_Pair[];
} = $props();
/* eslint-enable prefer-const */
let releasesPromise = $state(getAvailableVersions({}, { alertOnError: false }));
let existingPromise = $state(getExistingVersions({}, { alertOnError: false }));
let loadPromise = $derived(Promise.all([releasesPromise, existingPromise]));
const availableLangs = $derived(
langs.map((p) => ({ label: p.name, value: p.locale })),
);
const availableMirrors = $derived(
mirrors.map(({ mirror, name }) => ({
label: name,
value: mirror,
})),
);
// only the labels are expected to change
// svelte-ignore state_referenced_locally
let selectedMirror = $state(availableMirrors[0].value ?? Mirror.DISABLED);
let allowBetas: boolean = $state(options.allowBetas);
let downloadCaching: boolean = $state(options.downloadCaching);
const availableVersions = $derived(
$versionsStore?.all.map((v) => ({ label: v, value: v })) ?? [],
);
// const availableLatestVersions = $derived($versionsStore?.latest ?? []);
let selectedVersion = $derived(availableVersions[0]?.value);
/* eslint-disable prefer-const */
let currentVersion = $derived($versionsStore?.current);
let latestVersion = $derived($versionsStore?.latest[0]);
/* eslint-enable prefer-const */
let allowBetas = $state(options.allowBetas);
let downloadCaching = $state(options.downloadCaching);
let selectedMirror = $state(Mirror.DISABLED);
let choosePromise: Promise<ChooseVersionResponse | null> = $state(
Promise.resolve(null),
);
const choose = (version: string, keepExisting: boolean) => {
let error: Error | null = $state(null);
const setError = (e: Error) => {
error = e;
};
let term: Terminal | undefined = $state(undefined);
let termOpen = $state(false);
let chosen = $state(false);
const choose = (version: string, keepExisting: boolean, current?: string) => {
chosen = true;
term?.reset();
choosePromise = chooseVersion({
version,
keepExisting,
options: { allowBetas, downloadCaching, mirror: selectedMirror },
...(current ? { current } : {}),
});
};
let termRef: HTMLDivElement;
let termTabRef: HTMLDetailsElement;
onMount(() => {
const term = new Terminal({
disableStdin: true,
rows: 12,
cols: 60,
cursorStyle: "underline",
cursorInactiveStyle: "none",
// TODO: saw this in the docs, but do we need it?
windowsMode: navigator.platform.indexOf("Win") != -1,
});
term.open(termRef);
termRef.oncontextmenu = (e) => {
e.preventDefault();
term.selectAll();
const lines = term.getSelection().trim();
term.clearSelection();
navigator.clipboard.writeText(lines);
};
const unlisten = listen<string>("pty-data", (e) => {
const data = protoBase64.dec(e.payload);
if (!termTabRef.open) {
termTabRef.open = true;
}
term.write(data);
});
return () => {
term.dispose();
unlisten.then((cb) => cb());
};
});
// const zoomIn = () => ($zoomFactor = Math.min($zoomFactor + 0.1, 3));
// const zoomOut = () => ($zoomFactor = Math.max($zoomFactor - 0.1, 0.5));
</script>
<!-- TODO: this breaks scrolling on wsl, fine on win -->
<!-- <svelte:window -->
<!-- onwheel={(e) => { -->
<!-- if (!e.ctrlKey) { -->
<!-- return true; -->
<!-- } -->
<!-- e.preventDefault(); -->
<!-- e.deltaY < 0 ? zoomIn() : zoomOut(); -->
<!-- }} -->
<!-- /> -->
<Container
breakpoint="sm"
--gutter-inline="0.25rem"
--gutter-block="0.75rem"
class="container-columns"
>
{#key $currentLang}
<Row class="row-columns">
<TitledContainer title={""}>
<Row --cols={2} slot="title" class="title">
<img src="/anki.png" alt="logo" class="logo" />
<h1 class="title">{tr.launcherTitle()}</h1>
</Row>
<EnumSelectorRow
breakpoint="sm"
bind:value={selectedLang}
choices={availableLangs}
defaultValue={selectedLang}
hideRevert
>
<SettingTitle>
{tr.launcherLanguage()}
</SettingTitle>
</EnumSelectorRow>
<div class="group">
{#if latestVersion != null && latestVersion != currentVersion}
<Row class="centre m-3">
<button
class="btn btn-primary"
onclick={() => choose(latestVersion, false)}
>
{#if latestVersion == null}
{tr.launcherLatestAnki()}
{:else}
{tr.launcherLatestAnkiVersion({
version: latestVersion!,
})}
{/if}
</button>
</Row>
{/if}
{#if currentVersion != null}
<Row class="centre m-3">
<button
class="btn btn-primary"
onclick={() => choose(currentVersion, true)}
>
{tr.launcherKeepExistingVersion({
current: currentVersion ?? "N/A",
})}
</button>
</Row>
{/if}
<Row class="centre m-3">
<button
class="btn btn-primary"
onclick={() => choose(selectedVersion!, false)}
disabled={selectedVersion == null}
>
{tr.launcherChooseAVersion()}
</button>
<div class="m-2">
{"->"}
</div>
<div style="width: 100px">
{#if availableVersions.length !== 0}
<EnumSelector
bind:value={selectedVersion}
choices={availableVersions}
/>
{:else}
{"loading"}
{/if}
</div>
</Row>
</div>
</TitledContainer>
</Row>
{#await choosePromise}
<Warning warning={tr.launcherSyncing()} className="alert-info" />
{:then res}
{#if res != null}
<Warning
warning={tr.launcherAnkiWillStartShortly()}
className="alert-success"
/>
{/if}
{/await}
{/key}
<Row class="row-columns">
<details bind:this={termTabRef}>
{#key $currentLang}
<summary>{tr.launcherOutput()}</summary>
{/key}
<div id="terminal" bind:this={termRef}></div>
</details>
<TitledContainer>
<Row --cols={2} slot="title" class="title">
<img src="/anki.png" alt="logo" class="logo" />
<h1 class="title">{$tr.launcherTitle()}</h1>
</Row>
<EnumSelectorRow
breakpoint="sm"
bind:value={selectedLang}
choices={availableLangs}
defaultValue={selectedLang}
hideRevert
>
<SettingTitle>
{$tr.launcherLanguage()}
</SettingTitle>
</EnumSelectorRow>
{#await choosePromise}
<Row class="centre m-3">
<Spinner label={$tr.launcherSyncing()} />
</Row>
{:then res}
{#if res === null}
{#await loadPromise}
<Row class="centre m-3">
<Spinner label={$tr.launcherLoadingVersions()} />
</Row>
{:then [releases, existing]}
<Action {releases} {existing} {allowBetas} {choose} />
{:catch e}
{setError(e)}
<Warning
warning={$tr.lauuncherFailedToLoadVersions()}
className="alert-danger"
/>
{/await}
{:else}
<Row class="centre m-3">
<AnkiWillStart {res} />
</Row>
{/if}
{:catch e}
{setError(e)}
<Warning
warning={$tr.launcherFailedToSync()}
className="alert-danger"
/>
{/await}
{#if error != null}
<Row>
<pre>{error.message}</pre>
</Row>
{/if}
</TitledContainer>
</Row>
{#key $currentLang}
<Term bind:term bind:open={termOpen} />
{#if !chosen}
<Row class="row-columns">
<TitledContainer title={tr.launcherAdvanced()}>
<div class="m-2">
<SwitchRow
bind:value={allowBetas}
defaultValue={allowBetas}
hideRevert
>
<SettingTitle>
{tr.launcherAllowBetasToggle()}
</SettingTitle>
</SwitchRow>
</div>
<div class="m-2">
<SwitchRow
bind:value={downloadCaching}
defaultValue={downloadCaching}
hideRevert
>
<SettingTitle>
{tr.launcherDownloadCaching()}
</SettingTitle>
</SwitchRow>
</div>
<div class="m-2">
<EnumSelectorRow
breakpoint="sm"
bind:value={selectedMirror}
choices={availableMirrors}
defaultValue={selectedMirror}
hideRevert
>
<SettingTitle>
{tr.launcherUseMirror()}
</SettingTitle>
</EnumSelectorRow>
</div>
</TitledContainer>
<Options
{mirrors}
bind:allowBetas
bind:downloadCaching
bind:selectedMirror
/>
</Row>
{/key}
{/if}
</Container>
<style lang="scss">
@ -305,11 +183,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
justify-content: center;
}
.group {
margin-top: 1em;
}
#terminal {
width: 100%;
pre {
white-space: pre-wrap; /* Since CSS 2.1 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
</style>