mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 12:47:11 -05:00
add components
This commit is contained in:
parent
e398671e57
commit
3bb77f964d
5 changed files with 374 additions and 0 deletions
90
qt/launcher-gui/src/routes/Action.svelte
Normal file
90
qt/launcher-gui/src/routes/Action.svelte
Normal 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>
|
||||
65
qt/launcher-gui/src/routes/AnkiWillStart.svelte
Normal file
65
qt/launcher-gui/src/routes/AnkiWillStart.svelte
Normal 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}
|
||||
64
qt/launcher-gui/src/routes/Options.svelte
Normal file
64
qt/launcher-gui/src/routes/Options.svelte
Normal 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>
|
||||
70
qt/launcher-gui/src/routes/Spinner.svelte
Normal file
70
qt/launcher-gui/src/routes/Spinner.svelte
Normal 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>
|
||||
85
qt/launcher-gui/src/routes/Term.svelte
Normal file
85
qt/launcher-gui/src/routes/Term.svelte
Normal 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>
|
||||
Loading…
Reference in a new issue