add components

This commit is contained in:
llama 2025-10-18 10:07:18 +08:00
parent e398671e57
commit 3bb77f964d
No known key found for this signature in database
GPG key ID: 0B7543854B9413C3
5 changed files with 374 additions and 0 deletions

View file

@ -0,0 +1,90 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { tr } from "./stores";
import { ExistingVersions, Versions } from "@generated/anki/launcher_pb";
import Row from "$lib/components/Row.svelte";
import EnumSelector from "$lib/components/EnumSelector.svelte";
let {
releases,
existing,
allowBetas,
choose = (_, __) => {},
}: {
releases: Versions;
existing: ExistingVersions;
allowBetas: boolean;
choose: (version: string, existing: boolean, current?: string) => void;
} = $props();
const availableVersions = $derived(
releases.all
.filter((v) => allowBetas || !v.isPrerelease)
.map((v) => ({ label: v.version, value: v.version })),
);
let latest = $derived(availableVersions[0]?.value ?? null);
let selected = $derived(availableVersions[0]?.value ?? null);
let current = $derived(existing.current?.version);
let pyprojectModified = existing.pyprojectModifiedByUser;
function _choose(version: string, keepExisting: boolean = false) {
choose(version, keepExisting, current);
}
</script>
<div class="group">
{#if latest != null && latest != current}
<Row class="centre m-3">
<button class="btn btn-primary" onclick={() => _choose(latest)}>
{#if latest == null}
{$tr.launcherLatestAnki()}
{:else}
{$tr.launcherLatestAnkiVersion({
version: latest!,
})}
{/if}
</button>
</Row>
{/if}
{#if current != null}
<Row class="centre m-3">
<button class="btn btn-primary" onclick={() => _choose(current, true)}>
{#if pyprojectModified}
{$tr.launcherSyncProjectChanges()}
{:else}
{$tr.launcherKeepExistingVersion({ current })}
{/if}
</button>
</Row>
{/if}
<Row class="centre m-3">
<button
class="btn btn-primary"
onclick={() => _choose(selected!)}
disabled={selected == null}
>
{$tr.launcherChooseAVersion()}
</button>
<div class="m-2">
{"->"}
</div>
<div style="width: 100px">
<EnumSelector bind:value={selected} choices={availableVersions} />
</div>
</Row>
</div>
<style lang="scss">
:global(.centre) {
justify-content: center;
}
.group {
margin-top: 1em;
}
</style>

View file

@ -0,0 +1,65 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts" module>
let count = $state(3);
let timeout: any = $state(undefined);
let firstRun = $state(true);
let launch = $state(Promise.resolve({}));
</script>
<script lang="ts">
import { tr } from "./stores";
import Icon from "$lib/components/Icon.svelte";
import { checkDecagramOutline } from "$lib/components/icons";
import Warning from "./Warning.svelte";
import { onMount } from "svelte";
import { exit, launchAnki } from "@generated/backend-launcher";
import type { ChooseVersionResponse } from "@generated/anki/launcher_pb";
import IconConstrain from "$lib/components/IconConstrain.svelte";
import Spinner from "./Spinner.svelte";
const { res }: { res: ChooseVersionResponse } = $props();
const { warmingUp } = res;
if (firstRun) {
firstRun = false;
launch = launchAnki({});
const countdown = () => {
count -= 1;
if (count <= 0) {
exit({});
} else {
timeout = setTimeout(countdown, 1000);
}
};
if (!warmingUp) {
timeout = setTimeout(countdown, 1000);
onMount(() => {
return () => clearTimeout(timeout);
});
} else {
// wait for warm-up to end
launch.then(countdown);
}
}
</script>
{#await launch}
<Spinner>
<div>{$tr.launcherAnkiIsWarmingUp()}</div>
{#if warmingUp}
<div class="m-1">{$tr.launcherThisMayTake()}</div>
{/if}
</Spinner>
{:then}
<!-- TODO: replace with Spinner showing checkmark/cross -->
<Warning warning={$tr.launcherWillCloseIn({ count })} className="alert-success">
<IconConstrain iconSize={100} slot="icon">
<Icon icon={checkDecagramOutline} />
</IconConstrain>
</Warning>
{/await}

View file

@ -0,0 +1,64 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { tr } from "./stores";
import EnumSelectorRow from "$lib/components/EnumSelectorRow.svelte";
import SettingTitle from "$lib/components/SettingTitle.svelte";
import SwitchRow from "$lib/components/SwitchRow.svelte";
import TitledContainer from "$lib/components/TitledContainer.svelte";
import { Mirror } from "@generated/anki/launcher_pb";
let {
allowBetas = $bindable(),
downloadCaching = $bindable(),
mirrors,
selectedMirror = $bindable(),
} = $props();
const availableMirrors = $derived(
mirrors.map(({ mirror, name }) => ({
label: name,
value: mirror,
})),
);
// only the labels are expected to change
// svelte-ignore state_referenced_locally
selectedMirror = availableMirrors[0].value ?? Mirror.DISABLED;
</script>
<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>

View file

@ -0,0 +1,70 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { pageTheme } from "$lib/sveltelib/theme";
import { type Snippet } from "svelte";
let { label = "", children }: { label?: string; children?: Snippet } = $props();
</script>
<!-- spinner taken from https://loading.io/css/; CC0 -->
<div class="progress">
<div class="spinner" class:nightMode={$pageTheme.isDark}>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div id="label">
{label}
{@render children?.()}
</div>
</div>
<style lang="scss">
.spinner {
display: block;
position: relative;
width: 80px;
height: 80px;
margin: 0 auto;
div {
display: block;
position: absolute;
width: 32px;
height: 32px;
margin: 32px;
border: 2px solid #000;
border-radius: 50%;
animation: spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #000 transparent transparent transparent;
}
&.nightMode div {
border-top-color: #fff;
}
div:nth-child(1) {
animation-delay: -0.45s;
}
div:nth-child(2) {
animation-delay: -0.3s;
}
div:nth-child(3) {
animation-delay: -0.15s;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#label {
text-align: center;
}
</style>

View file

@ -0,0 +1,85 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { on } from "@tslib/events";
import { onMount } from "svelte";
import { protoBase64 } from "@bufbuild/protobuf";
import { listen } from "@tauri-apps/api/event";
import { Terminal } from "@xterm/xterm";
import { WebglAddon } from "@xterm/addon-webgl";
import "@xterm/xterm/css/xterm.css";
let {
term = $bindable(),
open = $bindable(false),
}: { term: Terminal | undefined; open: boolean } = $props();
let termRef: HTMLDivElement;
onMount(() => {
term = new Terminal({
fontFamily: '"Cascadia Code", Menlo, monospace',
disableStdin: true,
rows: 10,
cols: 50,
cursorStyle: "underline",
cursorInactiveStyle: "none",
altClickMovesCursor: false,
// TODO: saw this in the docs, but do we need it?
// windowsMode: navigator.platform.indexOf("Win") != -1,
});
// term.options.
term.open(termRef);
// dom renderer has viewport issues, try webgl
try {
const webgl = new WebglAddon();
term.loadAddon(webgl);
} catch (e) {
console.log("WebGL addon threw an exception during load", e);
}
const unlisten = listen<string>("pty-data", (e) => {
const data = protoBase64.dec(e.payload);
open = true;
term!.write(data);
});
// prevent wheel events from scrolling page if terminal has scrollback
const unsub = on(
document.querySelector(".xterm")! as HTMLElement,
"wheel",
(e) => {
if (term && term.buffer.active.baseY > 0) {
e.preventDefault();
}
},
);
return () => {
unlisten.then((cb) => cb());
unsub();
term!.dispose();
};
});
</script>
<div class="term-container centre" style:display={open ? "block" : "none"}>
<div class="term" bind:this={termRef}></div>
</div>
<style lang="scss">
.term-container {
display: flex;
margin: 20px 0 50px;
background-color: black;
border-radius: var(--border-radius-medium);
}
.term {
padding: 10px 20px;
}
</style>