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 License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "@generated/ftl-launcher";
import { import {
Mirror, Mirror,
type Options, type Options as OptionsProto,
type ChooseVersionResponse, type ChooseVersionResponse,
type GetLangsResponse_Pair, type GetLangsResponse_Pair,
type GetMirrorsResponse_Pair, type GetMirrorsResponse_Pair,
} from "@generated/anki/launcher_pb"; } 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 Row from "$lib/components/Row.svelte";
import EnumSelectorRow from "$lib/components/EnumSelectorRow.svelte"; import EnumSelectorRow from "$lib/components/EnumSelectorRow.svelte";
import SettingTitle from "$lib/components/SettingTitle.svelte"; import SettingTitle from "$lib/components/SettingTitle.svelte";
import TitledContainer from "$lib/components/TitledContainer.svelte"; import TitledContainer from "$lib/components/TitledContainer.svelte";
import Container from "$lib/components/Container.svelte"; import Container from "$lib/components/Container.svelte";
import SwitchRow from "$lib/components/SwitchRow.svelte"; import { tr } from "./stores";
import EnumSelector from "$lib/components/EnumSelector.svelte";
import Warning from "./Warning.svelte"; import Warning from "./Warning.svelte";
import { listen } from "@tauri-apps/api/event"; import Action from "./Action.svelte";
import { protoBase64 } from "@bufbuild/protobuf"; import Spinner from "./Spinner.svelte";
import { currentLang, versionsStore } from "./stores"; import Options from "./Options.svelte";
import { onMount } from "svelte"; import Term from "./Term.svelte";
import AnkiWillStart from "./AnkiWillStart.svelte";
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
// TODO: why
/* eslint-disable prefer-const */
let { let {
langs, langs,
selectedLang = $bindable(), selectedLang = $bindable(),
@ -37,241 +37,119 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}: { }: {
langs: GetLangsResponse_Pair[]; langs: GetLangsResponse_Pair[];
selectedLang: string; selectedLang: string;
options: Options; options: OptionsProto;
mirrors: GetMirrorsResponse_Pair[]; mirrors: GetMirrorsResponse_Pair[];
} = $props(); } = $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( const availableLangs = $derived(
langs.map((p) => ({ label: p.name, value: p.locale })), langs.map((p) => ({ label: p.name, value: p.locale })),
); );
const availableMirrors = $derived( let allowBetas = $state(options.allowBetas);
mirrors.map(({ mirror, name }) => ({ let downloadCaching = $state(options.downloadCaching);
label: name, let selectedMirror = $state(Mirror.DISABLED);
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 choosePromise: Promise<ChooseVersionResponse | null> = $state( let choosePromise: Promise<ChooseVersionResponse | null> = $state(
Promise.resolve(null), 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({ choosePromise = chooseVersion({
version, version,
keepExisting, keepExisting,
options: { allowBetas, downloadCaching, mirror: selectedMirror }, 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> </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 <Container
breakpoint="sm" breakpoint="sm"
--gutter-inline="0.25rem" --gutter-inline="0.25rem"
--gutter-block="0.75rem" --gutter-block="0.75rem"
class="container-columns" 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"> <Row class="row-columns">
<details bind:this={termTabRef}> <TitledContainer>
{#key $currentLang} <Row --cols={2} slot="title" class="title">
<summary>{tr.launcherOutput()}</summary> <img src="/anki.png" alt="logo" class="logo" />
{/key} <h1 class="title">{$tr.launcherTitle()}</h1>
<div id="terminal" bind:this={termRef}></div> </Row>
</details> <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> </Row>
{#key $currentLang} <Term bind:term bind:open={termOpen} />
{#if !chosen}
<Row class="row-columns"> <Row class="row-columns">
<TitledContainer title={tr.launcherAdvanced()}> <Options
<div class="m-2"> {mirrors}
<SwitchRow bind:allowBetas
bind:value={allowBetas} bind:downloadCaching
defaultValue={allowBetas} bind:selectedMirror
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>
</Row> </Row>
{/key} {/if}
</Container> </Container>
<style lang="scss"> <style lang="scss">
@ -305,11 +183,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
justify-content: center; justify-content: center;
} }
.group { pre {
margin-top: 1em; white-space: pre-wrap; /* Since CSS 2.1 */
} white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
#terminal { white-space: -o-pre-wrap; /* Opera 7 */
width: 100%; word-wrap: break-word; /* Internet Explorer 5.5+ */
} }
</style> </style>