mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 22:42:25 -04:00
Merge pull request #1084 from hgiesel/svelte-hooks
Svelte "hooks" - Streamline method of getting async data
This commit is contained in:
commit
1bf19d581f
12 changed files with 212 additions and 103 deletions
|
@ -20,6 +20,7 @@ ts_library(
|
|||
"GraphsPage",
|
||||
"lib",
|
||||
"//ts/lib",
|
||||
"//ts/sveltelib",
|
||||
"@npm//svelte",
|
||||
"@npm//svelte2tsx",
|
||||
],
|
||||
|
@ -32,6 +33,7 @@ ts_library(
|
|||
exclude = ["index.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//ts/sveltelib",
|
||||
"//ts/lib",
|
||||
"//ts/lib:backend_proto",
|
||||
"@npm//@types/d3",
|
||||
|
@ -59,6 +61,7 @@ esbuild(
|
|||
output_css = True,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//ts/sveltelib",
|
||||
"//ts/lib",
|
||||
"//ts/lib:backend_proto",
|
||||
"//ts/lib:fluent_proto",
|
||||
|
|
|
@ -43,6 +43,10 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
@ -57,7 +61,7 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<div class="graph">
|
||||
<div class="graph" tabindex="-1">
|
||||
<h1>{title}</h1>
|
||||
|
||||
{#if subtitle}
|
||||
|
|
|
@ -2,82 +2,49 @@
|
|||
import "../sass/core.css";
|
||||
|
||||
import type { SvelteComponent } from "svelte/internal";
|
||||
import { writable } from "svelte/store";
|
||||
import type { I18n } from "anki/i18n";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
import type pb from "anki/backend_proto";
|
||||
import { getGraphData, RevlogRange, daysToRevlogRange } from "./graph-helpers";
|
||||
import { getPreferences } from "./preferences";
|
||||
import { bridgeCommand } from "anki/bridgecommand";
|
||||
|
||||
import WithGraphData from "./WithGraphData.svelte";
|
||||
|
||||
export let i18n: I18n;
|
||||
export let nightMode: boolean;
|
||||
export let graphs: SvelteComponent[];
|
||||
|
||||
export let search: string;
|
||||
export let days: number;
|
||||
export let initialSearch: string;
|
||||
export let initialDays: number;
|
||||
export let controller: SvelteComponent | null;
|
||||
|
||||
let active = false;
|
||||
let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
let preferences: PreferenceStore | null = null;
|
||||
let revlogRange: RevlogRange;
|
||||
const search = writable(initialSearch);
|
||||
const days = writable(initialDays);
|
||||
|
||||
const preferencesPromise = getPreferences();
|
||||
|
||||
const refreshWith = async (searchNew: string, days: number) => {
|
||||
search = searchNew;
|
||||
|
||||
active = true;
|
||||
try {
|
||||
[sourceData, preferences] = await Promise.all([
|
||||
getGraphData(search, days),
|
||||
preferencesPromise,
|
||||
]);
|
||||
revlogRange = daysToRevlogRange(days);
|
||||
} catch (e) {
|
||||
sourceData = null;
|
||||
alert(e);
|
||||
function browserSearch(event: CustomEvent) {
|
||||
bridgeCommand(`browserSearch: ${$search} ${event.detail.query}`);
|
||||
}
|
||||
active = false;
|
||||
};
|
||||
|
||||
const refresh = (event: CustomEvent) => {
|
||||
refreshWith(event.detail.search, event.detail.days);
|
||||
};
|
||||
|
||||
refreshWith(search, days);
|
||||
|
||||
const browserSearch = (event: CustomEvent) => {
|
||||
const query = `${search} ${event.detail.query}`;
|
||||
bridgeCommand(`browserSearch:${query}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
@media only screen and (max-width: 600px) {
|
||||
.base {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-focus-outline:focus {
|
||||
outline: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="base">
|
||||
{#if controller}
|
||||
<svelte:component
|
||||
this={controller}
|
||||
{i18n}
|
||||
<div>
|
||||
<WithGraphData
|
||||
{search}
|
||||
{days}
|
||||
{active}
|
||||
on:update={refresh} />
|
||||
let:loading
|
||||
let:sourceData
|
||||
let:preferences
|
||||
let:revlogRange>
|
||||
{#if controller}
|
||||
<svelte:component this={controller} {i18n} {search} {days} {loading} />
|
||||
{/if}
|
||||
|
||||
{#if sourceData}
|
||||
<div tabindex="-1" class="no-focus-outline">
|
||||
{#if sourceData && preferences && revlogRange}
|
||||
{#each graphs as graph}
|
||||
<svelte:component
|
||||
this={graph}
|
||||
|
@ -88,6 +55,6 @@
|
|||
{nightMode}
|
||||
on:search={browserSearch} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</WithGraphData>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="typescript">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
import InputBox from "./InputBox.svelte";
|
||||
|
||||
|
@ -12,45 +12,29 @@
|
|||
Custom = 3,
|
||||
}
|
||||
|
||||
type UpdateEventMap = {
|
||||
update: { days: number; search: string; searchRange: SearchRange };
|
||||
};
|
||||
|
||||
export let i18n: I18n;
|
||||
export let active: boolean;
|
||||
export let loading: boolean;
|
||||
|
||||
export let days: number;
|
||||
export let search: string;
|
||||
export let days: Writable<number>;
|
||||
export let search: Writable<string>;
|
||||
|
||||
const dispatch = createEventDispatcher<UpdateEventMap>();
|
||||
|
||||
let revlogRange = daysToRevlogRange(days);
|
||||
let searchRange: SearchRange =
|
||||
search === "deck:current"
|
||||
let revlogRange = daysToRevlogRange($days);
|
||||
let searchRange =
|
||||
$search === "deck:current"
|
||||
? SearchRange.Deck
|
||||
: search === ""
|
||||
: $search === ""
|
||||
? SearchRange.Collection
|
||||
: SearchRange.Custom;
|
||||
|
||||
let displayedSearch = search;
|
||||
|
||||
const update = () => {
|
||||
dispatch("update", {
|
||||
days: days,
|
||||
search: search,
|
||||
searchRange: searchRange,
|
||||
});
|
||||
};
|
||||
let displayedSearch = $search;
|
||||
|
||||
$: {
|
||||
switch (searchRange as SearchRange) {
|
||||
case SearchRange.Deck:
|
||||
search = displayedSearch = "deck:current";
|
||||
update();
|
||||
$search = displayedSearch = "deck:current";
|
||||
break;
|
||||
case SearchRange.Collection:
|
||||
search = displayedSearch = "";
|
||||
update();
|
||||
$search = displayedSearch = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -58,23 +42,20 @@
|
|||
$: {
|
||||
switch (revlogRange as RevlogRange) {
|
||||
case RevlogRange.Year:
|
||||
days = 365;
|
||||
update();
|
||||
$days = 365;
|
||||
break;
|
||||
case RevlogRange.All:
|
||||
days = 0;
|
||||
update();
|
||||
$days = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const searchKeyUp = (e: KeyboardEvent) => {
|
||||
function searchKeyUp(event: KeyboardEvent): void {
|
||||
// fetch data on enter
|
||||
if (e.key == "Enter") {
|
||||
search = displayedSearch;
|
||||
update();
|
||||
if (event.code === "Enter") {
|
||||
$search = displayedSearch;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const year = i18n.tr(i18n.TR.STATISTICS_RANGE_1_YEAR_HISTORY);
|
||||
const deck = i18n.tr(i18n.TR.STATISTICS_RANGE_DECK);
|
||||
|
@ -117,7 +98,7 @@
|
|||
|
||||
opacity: 0;
|
||||
|
||||
&.active {
|
||||
&.loading {
|
||||
opacity: 0.5;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
|
@ -129,7 +110,7 @@
|
|||
</style>
|
||||
|
||||
<div class="range-box">
|
||||
<div class="spin" class:active>◐</div>
|
||||
<div class="spin" class:loading>◐</div>
|
||||
|
||||
<InputBox>
|
||||
<label>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
let container = (null as unknown) as HTMLDivElement;
|
||||
|
||||
let adjustedX, adjustedY: number;
|
||||
let adjustedX: number, adjustedY: number;
|
||||
|
||||
let shiftLeftAmount = 0;
|
||||
$: shiftLeftAmount = container
|
||||
|
|
44
ts/graphs/WithGraphData.svelte
Normal file
44
ts/graphs/WithGraphData.svelte
Normal file
|
@ -0,0 +1,44 @@
|
|||
<script lang="typescript">
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
import useAsync from "sveltelib/async";
|
||||
import useAsyncReactive from "sveltelib/asyncReactive";
|
||||
|
||||
import { getGraphData, daysToRevlogRange } from "./graph-helpers";
|
||||
import { getPreferences } from "./preferences";
|
||||
|
||||
export let search: Writable<string>;
|
||||
export let days: Writable<number>;
|
||||
|
||||
const {
|
||||
loading: graphLoading,
|
||||
error: graphError,
|
||||
value: graphValue,
|
||||
} = useAsyncReactive(() => getGraphData($search, $days), [search, days]);
|
||||
|
||||
const {
|
||||
loading: prefsLoading,
|
||||
error: prefsError,
|
||||
value: prefsValue,
|
||||
} = useAsync(() => getPreferences());
|
||||
|
||||
$: revlogRange = daysToRevlogRange($days);
|
||||
|
||||
$: {
|
||||
if ($graphError) {
|
||||
alert($graphError);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($prefsError) {
|
||||
alert($prefsError);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot
|
||||
{revlogRange}
|
||||
loading={$graphLoading || $prefsLoading}
|
||||
sourceData={$graphValue}
|
||||
preferences={$prefsValue} />
|
|
@ -40,8 +40,8 @@ export function graphs(
|
|||
i18n,
|
||||
graphs,
|
||||
nightMode,
|
||||
search,
|
||||
days,
|
||||
initialSearch: search,
|
||||
initialDays: days,
|
||||
controller,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -67,6 +67,7 @@ def svelte_check(name = "svelte_check", srcs = []):
|
|||
],
|
||||
data = [
|
||||
"//ts:tsconfig.json",
|
||||
"//ts/sveltelib",
|
||||
"//ts/lib",
|
||||
"//ts/lib:backend_proto",
|
||||
"@npm//sass",
|
||||
|
|
32
ts/sveltelib/BUILD.bazel
Normal file
32
ts/sveltelib/BUILD.bazel
Normal file
|
@ -0,0 +1,32 @@
|
|||
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||
load("//ts:prettier.bzl", "prettier_test")
|
||||
load("//ts:eslint.bzl", "eslint_test")
|
||||
|
||||
# Anki Library
|
||||
################
|
||||
|
||||
ts_library(
|
||||
name = "sveltelib",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
data = [],
|
||||
module_name = "sveltelib",
|
||||
tsconfig = "//:tsconfig.json",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"@npm//svelte",
|
||||
"@npm//tslib",
|
||||
],
|
||||
)
|
||||
|
||||
# Tests
|
||||
################
|
||||
|
||||
prettier_test(
|
||||
name = "format_check",
|
||||
srcs = glob(["*.ts"]),
|
||||
)
|
||||
|
||||
eslint_test(
|
||||
name = "eslint",
|
||||
srcs = glob(["*.ts"]),
|
||||
)
|
27
ts/sveltelib/async.ts
Normal file
27
ts/sveltelib/async.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Readable, readable } from "svelte/store";
|
||||
|
||||
interface AsyncData<T, E> {
|
||||
value: Readable<T | null>;
|
||||
error: Readable<E | null>;
|
||||
loading: Readable<boolean>;
|
||||
}
|
||||
|
||||
function useAsync<T, E = unknown>(asyncFunction: () => Promise<T>): AsyncData<T, E> {
|
||||
const promise = asyncFunction();
|
||||
|
||||
const value = readable(null, (set: (value: T) => void) => {
|
||||
promise.then((value: T) => set(value));
|
||||
});
|
||||
|
||||
const error = readable(null, (set: (value: E) => void) => {
|
||||
promise.catch((value: E) => set(value));
|
||||
});
|
||||
|
||||
const loading = readable(true, (set: (value: boolean) => void) => {
|
||||
promise.finally(() => set(false));
|
||||
});
|
||||
|
||||
return { value, error, loading };
|
||||
}
|
||||
|
||||
export default useAsync;
|
49
ts/sveltelib/asyncReactive.ts
Normal file
49
ts/sveltelib/asyncReactive.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { Readable, derived } from "svelte/store";
|
||||
|
||||
interface AsyncReativeData<T, E> {
|
||||
value: Readable<T | null>;
|
||||
error: Readable<E | null>;
|
||||
loading: Readable<boolean>;
|
||||
}
|
||||
|
||||
function useAsyncReactive<T, E>(
|
||||
asyncFunction: () => Promise<T>,
|
||||
dependencies: [Readable<unknown>, ...Readable<unknown>[]]
|
||||
): AsyncReativeData<T, E> {
|
||||
const promise = derived(
|
||||
dependencies,
|
||||
(_, set: (value: Promise<T> | null) => void): void => set(asyncFunction()),
|
||||
// initialize with null to avoid duplicate fetch on init
|
||||
null
|
||||
);
|
||||
|
||||
const value = derived(
|
||||
promise,
|
||||
($promise, set: (value: T) => void): void => {
|
||||
$promise?.then((value: T) => set(value));
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
const error = derived(
|
||||
promise,
|
||||
($promise, set: (error: E | null) => void): (() => void) => {
|
||||
$promise?.catch((error: E) => set(error));
|
||||
return (): void => set(null);
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
const loading = derived(
|
||||
promise,
|
||||
($promise, set: (value: boolean) => void): (() => void) => {
|
||||
$promise?.finally(() => set(false));
|
||||
return (): void => set(true);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return { value, error, loading };
|
||||
}
|
||||
|
||||
export default useAsyncReactive;
|
|
@ -3,10 +3,11 @@
|
|||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "es6",
|
||||
"lib": ["es2017", "es2019.array", "dom", "dom.iterable"],
|
||||
"lib": ["es2017", "es2019.array", "es2018.promise", "dom", "dom.iterable"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"anki/*": ["../bazel-bin/ts/lib/*"]
|
||||
"anki/*": ["../bazel-bin/ts/lib/*"],
|
||||
"sveltelib/*": ["../bazel-bin/ts/sveltelib/*"]
|
||||
},
|
||||
"importsNotUsedAsValues": "error",
|
||||
"outDir": "dist",
|
||||
|
|
Loading…
Reference in a new issue