mirror of
https://github.com/ankitects/anki.git
synced 2025-11-15 17:17:11 -05:00
Merge pull request #864 from hgiesel/graphs
Decouple GraphsPage from individual graphs and RangeBox
This commit is contained in:
commit
be7556629b
6 changed files with 209 additions and 133 deletions
|
|
@ -30,6 +30,7 @@ ts_library(
|
||||||
"GraphsPage",
|
"GraphsPage",
|
||||||
"lib",
|
"lib",
|
||||||
"//ts/lib",
|
"//ts/lib",
|
||||||
|
"@npm//svelte",
|
||||||
"@npm//svelte2tsx",
|
"@npm//svelte2tsx",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,156 +3,61 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
|
import type { SvelteComponent } from "svelte/internal";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import { getGraphData, RevlogRange } from "./graph-helpers";
|
import { getGraphData, RevlogRange } from "./graph-helpers";
|
||||||
import IntervalsGraph from "./IntervalsGraph.svelte";
|
|
||||||
import EaseGraph from "./EaseGraph.svelte";
|
|
||||||
import AddedGraph from "./AddedGraph.svelte";
|
|
||||||
import TodayStats from "./TodayStats.svelte";
|
|
||||||
import ButtonsGraph from "./ButtonsGraph.svelte";
|
|
||||||
import CardCounts from "./CardCounts.svelte";
|
|
||||||
import HourGraph from "./HourGraph.svelte";
|
|
||||||
import FutureDue from "./FutureDue.svelte";
|
|
||||||
import ReviewsGraph from "./ReviewsGraph.svelte";
|
|
||||||
import CalendarGraph from "./CalendarGraph.svelte";
|
|
||||||
|
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
export let nightMode: boolean;
|
export let nightMode: boolean;
|
||||||
|
export let graphs: SvelteComponent[];
|
||||||
|
|
||||||
|
export let search: string;
|
||||||
|
export let days: number;
|
||||||
|
export let controller: SvelteComponent;
|
||||||
|
|
||||||
|
let active = false;
|
||||||
let sourceData: pb.BackendProto.GraphsOut | null = null;
|
let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
let revlogRange: RevlogRange;
|
||||||
|
|
||||||
enum SearchRange {
|
const refreshWith = async (search: string, days: number) => {
|
||||||
Deck = 1,
|
active = true;
|
||||||
Collection = 2,
|
|
||||||
Custom = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
let searchRange: SearchRange = SearchRange.Deck;
|
|
||||||
let revlogRange: RevlogRange = RevlogRange.Year;
|
|
||||||
let days: number = 31;
|
|
||||||
let refreshing = false;
|
|
||||||
|
|
||||||
let search = "deck:current";
|
|
||||||
let displayedSearch = search;
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
refreshing = true;
|
|
||||||
try {
|
try {
|
||||||
sourceData = await getGraphData(search, days);
|
sourceData = await getGraphData(search, days);
|
||||||
|
revlogRange = days > 365 || days === 0 ? RevlogRange.All : RevlogRange.Year;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sourceData = null;
|
sourceData = null;
|
||||||
alert(i18n.tr(i18n.TR.STATISTICS_ERROR_FETCHING));
|
alert(i18n.tr(i18n.TR.STATISTICS_ERROR_FETCHING));
|
||||||
}
|
}
|
||||||
refreshing = false;
|
active = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
$: {
|
const refresh = (event: CustomEvent) => {
|
||||||
// refresh if either of these change
|
refreshWith(event.detail.search, event.detail.days);
|
||||||
search;
|
|
||||||
days;
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
switch (searchRange as SearchRange) {
|
|
||||||
case SearchRange.Deck:
|
|
||||||
search = displayedSearch = "deck:current";
|
|
||||||
break;
|
|
||||||
case SearchRange.Collection:
|
|
||||||
search = displayedSearch = "";
|
|
||||||
break;
|
|
||||||
case SearchRange.Custom:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
switch (revlogRange as RevlogRange) {
|
|
||||||
case RevlogRange.Year:
|
|
||||||
days = 365;
|
|
||||||
break;
|
|
||||||
case RevlogRange.All:
|
|
||||||
days = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchKeyUp = (e: KeyboardEvent) => {
|
|
||||||
// fetch data on enter
|
|
||||||
if (e.key == "Enter") {
|
|
||||||
const wasSame = search == displayedSearch;
|
|
||||||
search = displayedSearch;
|
|
||||||
if (wasSame) {
|
|
||||||
// force a refresh (user may have changed current deck, etc)
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const year = i18n.tr(i18n.TR.STATISTICS_RANGE_1_YEAR_HISTORY);
|
refreshWith(search, days);
|
||||||
const deck = i18n.tr(i18n.TR.STATISTICS_RANGE_DECK);
|
|
||||||
const collection = i18n.tr(i18n.TR.STATISTICS_RANGE_COLLECTION);
|
|
||||||
const searchLabel = i18n.tr(i18n.TR.STATISTICS_RANGE_SEARCH);
|
|
||||||
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_HISTORY);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
{#if controller}
|
||||||
.no-focus-outline:focus {
|
<svelte:component
|
||||||
outline: 0;
|
this={controller}
|
||||||
}
|
{i18n}
|
||||||
</style>
|
{search}
|
||||||
|
{days}
|
||||||
|
{active}
|
||||||
|
on:update={refresh} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="range-box">
|
{#if sourceData}
|
||||||
<div class="spin {refreshing ? 'active' : ''}">◐</div>
|
<div tabindex="-1" class="no-focus-outline">
|
||||||
|
{#each graphs as graph}
|
||||||
<div class="range-box-inner">
|
<svelte:component
|
||||||
<label>
|
this={graph}
|
||||||
<input type="radio" bind:group={searchRange} value={SearchRange.Deck} />
|
{sourceData}
|
||||||
{deck}
|
{revlogRange}
|
||||||
</label>
|
{i18n}
|
||||||
<label>
|
{nightMode} />
|
||||||
<input
|
{/each}
|
||||||
type="radio"
|
|
||||||
bind:group={searchRange}
|
|
||||||
value={SearchRange.Collection} />
|
|
||||||
{collection}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
bind:value={displayedSearch}
|
|
||||||
on:keyup={searchKeyUp}
|
|
||||||
on:focus={() => {
|
|
||||||
searchRange = SearchRange.Custom;
|
|
||||||
}}
|
|
||||||
placeholder={searchLabel} />
|
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="range-box-inner">
|
|
||||||
<label>
|
|
||||||
<input type="radio" bind:group={revlogRange} value={RevlogRange.Year} />
|
|
||||||
{year}
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" bind:group={revlogRange} value={RevlogRange.All} />
|
|
||||||
{all}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="range-box-pad" />
|
|
||||||
|
|
||||||
<div tabindex="-1" class="no-focus-outline">
|
|
||||||
{#if sourceData}
|
|
||||||
<TodayStats {sourceData} {i18n} />
|
|
||||||
<FutureDue {sourceData} {i18n} />
|
|
||||||
<CalendarGraph {sourceData} {revlogRange} {i18n} {nightMode} />
|
|
||||||
<ReviewsGraph {sourceData} {revlogRange} {i18n} />
|
|
||||||
<CardCounts {sourceData} {i18n} />
|
|
||||||
<IntervalsGraph {sourceData} {i18n} />
|
|
||||||
<EaseGraph {sourceData} {i18n} />
|
|
||||||
<HourGraph {sourceData} {revlogRange} {i18n} />
|
|
||||||
<ButtonsGraph {sourceData} {revlogRange} {i18n} />
|
|
||||||
<AddedGraph {sourceData} {i18n} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
|
||||||
120
ts/graphs/RangeBox.svelte
Normal file
120
ts/graphs/RangeBox.svelte
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<script lang="typescript">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import type { I18n } from "anki/i18n";
|
||||||
|
import { RevlogRange } from "./graph-helpers";
|
||||||
|
|
||||||
|
enum SearchRange {
|
||||||
|
Deck = 1,
|
||||||
|
Collection = 2,
|
||||||
|
Custom = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export let i18n: I18n;
|
||||||
|
export let active: boolean;
|
||||||
|
|
||||||
|
export let days: number;
|
||||||
|
export let search: string;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let revlogRange: RevlogRange =
|
||||||
|
days > 365 || days === 0 ? RevlogRange.Year : RevlogRange.All;
|
||||||
|
|
||||||
|
let searchRange: SearchRange =
|
||||||
|
search === "deck:current"
|
||||||
|
? SearchRange.Deck
|
||||||
|
: search === ""
|
||||||
|
? SearchRange.Collection
|
||||||
|
: SearchRange.Custom;
|
||||||
|
|
||||||
|
let displayedSearch = search;
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
dispatch("update", {
|
||||||
|
days: days,
|
||||||
|
search: search,
|
||||||
|
searchRange: searchRange,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$: {
|
||||||
|
switch (searchRange as SearchRange) {
|
||||||
|
case SearchRange.Deck:
|
||||||
|
search = displayedSearch = "deck:current";
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
case SearchRange.Collection:
|
||||||
|
search = displayedSearch = "";
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
switch (revlogRange as RevlogRange) {
|
||||||
|
case RevlogRange.Year:
|
||||||
|
days = 365;
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
case RevlogRange.All:
|
||||||
|
days = 0;
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchKeyUp = (e: KeyboardEvent) => {
|
||||||
|
// fetch data on enter
|
||||||
|
if (e.key == "Enter") {
|
||||||
|
search = displayedSearch;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const year = i18n.tr(i18n.TR.STATISTICS_RANGE_1_YEAR_HISTORY);
|
||||||
|
const deck = i18n.tr(i18n.TR.STATISTICS_RANGE_DECK);
|
||||||
|
const collection = i18n.tr(i18n.TR.STATISTICS_RANGE_COLLECTION);
|
||||||
|
const searchLabel = i18n.tr(i18n.TR.STATISTICS_RANGE_SEARCH);
|
||||||
|
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_HISTORY);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="range-box">
|
||||||
|
<div class="spin" class:active>◐</div>
|
||||||
|
|
||||||
|
<div class="range-box-inner">
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={searchRange} value={SearchRange.Deck} />
|
||||||
|
{deck}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
bind:group={searchRange}
|
||||||
|
value={SearchRange.Collection} />
|
||||||
|
{collection}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={displayedSearch}
|
||||||
|
on:keyup={searchKeyUp}
|
||||||
|
on:focus={() => {
|
||||||
|
searchRange = SearchRange.Custom;
|
||||||
|
}}
|
||||||
|
placeholder={searchLabel} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="range-box-inner">
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={revlogRange} value={RevlogRange.Year} />
|
||||||
|
{year}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={revlogRange} value={RevlogRange.All} />
|
||||||
|
{all}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="range-box-pad" />
|
||||||
|
|
@ -1,15 +1,44 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// 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
|
||||||
|
|
||||||
|
import type { SvelteComponent } from "svelte/internal";
|
||||||
|
|
||||||
import { setupI18n } from "anki/i18n";
|
import { setupI18n } from "anki/i18n";
|
||||||
import GraphsPage from "./GraphsPage.svelte";
|
import GraphsPage from "./GraphsPage.svelte";
|
||||||
import { checkNightMode } from "anki/nightmode";
|
import { checkNightMode } from "anki/nightmode";
|
||||||
|
|
||||||
export function graphs(target: HTMLDivElement): void {
|
export { default as RangeBox } from "./RangeBox.svelte";
|
||||||
|
|
||||||
|
export { default as IntervalsGraph } from "./IntervalsGraph.svelte";
|
||||||
|
export { default as EaseGraph } from "./EaseGraph.svelte";
|
||||||
|
export { default as AddedGraph } from "./AddedGraph.svelte";
|
||||||
|
export { default as TodayStats } from "./TodayStats.svelte";
|
||||||
|
export { default as ButtonsGraph } from "./ButtonsGraph.svelte";
|
||||||
|
export { default as CardCounts } from "./CardCounts.svelte";
|
||||||
|
export { default as HourGraph } from "./HourGraph.svelte";
|
||||||
|
export { default as FutureDue } from "./FutureDue.svelte";
|
||||||
|
export { default as ReviewsGraph } from "./ReviewsGraph.svelte";
|
||||||
|
export { default as CalendarGraph } from "./CalendarGraph.svelte";
|
||||||
|
export { RevlogRange } from "./graph-helpers";
|
||||||
|
|
||||||
|
export function graphs(
|
||||||
|
target: HTMLDivElement,
|
||||||
|
graphs: SvelteComponent[],
|
||||||
|
{ search = "deck:current", days = 365, controller = null } = {}
|
||||||
|
): void {
|
||||||
|
const nightMode = checkNightMode();
|
||||||
|
|
||||||
setupI18n().then((i18n) => {
|
setupI18n().then((i18n) => {
|
||||||
new GraphsPage({
|
new GraphsPage({
|
||||||
target,
|
target,
|
||||||
props: { i18n, nightMode: checkNightMode() },
|
props: {
|
||||||
|
i18n,
|
||||||
|
graphs,
|
||||||
|
nightMode,
|
||||||
|
search,
|
||||||
|
days,
|
||||||
|
controller,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,23 @@
|
||||||
<script src="../js/vendor/protobuf.min.js"></script>
|
<script src="../js/vendor/protobuf.min.js"></script>
|
||||||
<script src="graphs.js"></script>
|
<script src="graphs.js"></script>
|
||||||
<script>
|
<script>
|
||||||
anki.graphs(document.getElementById("main"));
|
anki.graphs(
|
||||||
|
document.getElementById("main"),
|
||||||
|
[
|
||||||
|
anki.TodayStats,
|
||||||
|
anki.FutureDue,
|
||||||
|
anki.CalendarGraph,
|
||||||
|
anki.ReviewsGraph,
|
||||||
|
anki.CardCounts,
|
||||||
|
anki.IntervalsGraph,
|
||||||
|
anki.EaseGraph,
|
||||||
|
anki.HourGraph,
|
||||||
|
anki.ButtonsGraph,
|
||||||
|
anki.AddedGraph,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
controller: anki.RangeBox,
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -193,3 +193,7 @@
|
||||||
.align-start {
|
.align-start {
|
||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-focus-outline:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue