mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Merge pull request #934 from hgiesel/graphprefs
Add GraphsPreferences API to graphs for setting persistent preferences
This commit is contained in:
commit
2a875ffc55
15 changed files with 231 additions and 57 deletions
|
@ -34,7 +34,7 @@ LABEL_REPEATED = 3
|
||||||
|
|
||||||
# messages we don't want to unroll in codegen
|
# messages we don't want to unroll in codegen
|
||||||
SKIP_UNROLL_INPUT = {"TranslateString"}
|
SKIP_UNROLL_INPUT = {"TranslateString"}
|
||||||
SKIP_DECODE = {"Graphs"}
|
SKIP_DECODE = {"Graphs", "GetGraphPreferences"}
|
||||||
|
|
||||||
|
|
||||||
def python_type(field):
|
def python_type(field):
|
||||||
|
|
|
@ -18,6 +18,7 @@ import flask_cors # type: ignore
|
||||||
from flask import Response, request
|
from flask import Response, request
|
||||||
from waitress.server import create_server
|
from waitress.server import create_server
|
||||||
|
|
||||||
|
import anki.backend_pb2 as pb
|
||||||
import aqt
|
import aqt
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.rsbackend import from_json_bytes
|
from anki.rsbackend import from_json_bytes
|
||||||
|
@ -255,17 +256,29 @@ def graph_data() -> bytes:
|
||||||
return aqt.mw.col.backend.graphs(search=args["search"], days=args["days"])
|
return aqt.mw.col.backend.graphs(search=args["search"], days=args["days"])
|
||||||
|
|
||||||
|
|
||||||
|
def graph_preferences() -> bytes:
|
||||||
|
return aqt.mw.col.backend.get_graph_preferences()
|
||||||
|
|
||||||
|
|
||||||
|
def set_graph_preferences() -> None:
|
||||||
|
input = pb.GraphPreferences()
|
||||||
|
input.ParseFromString(request.data)
|
||||||
|
aqt.mw.col.backend.set_graph_preferences(input=input)
|
||||||
|
|
||||||
|
|
||||||
def congrats_info() -> bytes:
|
def congrats_info() -> bytes:
|
||||||
info = aqt.mw.col.backend.congrats_info()
|
info = aqt.mw.col.backend.congrats_info()
|
||||||
return info.SerializeToString()
|
return info.SerializeToString()
|
||||||
|
|
||||||
|
|
||||||
post_handlers = dict(
|
post_handlers = {
|
||||||
graphData=graph_data,
|
"graphData": graph_data,
|
||||||
|
"graphPreferences": graph_preferences,
|
||||||
|
"setGraphPreferences": set_graph_preferences,
|
||||||
# pylint: disable=unnecessary-lambda
|
# pylint: disable=unnecessary-lambda
|
||||||
i18nResources=lambda: aqt.mw.col.backend.i18n_resources(),
|
"i18nResources": lambda: aqt.mw.col.backend.i18n_resources(),
|
||||||
congratsInfo=congrats_info,
|
"congratsInfo": congrats_info,
|
||||||
)
|
}
|
||||||
|
|
||||||
|
|
||||||
def handle_post(path: str) -> Response:
|
def handle_post(path: str) -> Response:
|
||||||
|
@ -274,12 +287,15 @@ def handle_post(path: str) -> Response:
|
||||||
return flask.make_response("Collection not open", HTTPStatus.NOT_FOUND)
|
return flask.make_response("Collection not open", HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
if path in post_handlers:
|
if path in post_handlers:
|
||||||
data = post_handlers[path]()
|
if data := post_handlers[path]():
|
||||||
response = flask.make_response(data)
|
response = flask.make_response(data)
|
||||||
response.headers["Content-Type"] = "application/binary"
|
response.headers["Content-Type"] = "application/binary"
|
||||||
return response
|
else:
|
||||||
|
response = flask.make_response("", HTTPStatus.NO_CONTENT)
|
||||||
else:
|
else:
|
||||||
return flask.make_response(
|
response = flask.make_response(
|
||||||
f"Unhandled post to {path}",
|
f"Unhandled post to {path}",
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
|
@ -116,6 +116,8 @@ service BackendService {
|
||||||
|
|
||||||
rpc CardStats(CardID) returns (String);
|
rpc CardStats(CardID) returns (String);
|
||||||
rpc Graphs(GraphsIn) returns (GraphsOut);
|
rpc Graphs(GraphsIn) returns (GraphsOut);
|
||||||
|
rpc GetGraphPreferences(Empty) returns (GraphPreferences);
|
||||||
|
rpc SetGraphPreferences(GraphPreferences) returns (Empty);
|
||||||
|
|
||||||
// media
|
// media
|
||||||
|
|
||||||
|
@ -1082,12 +1084,6 @@ message GraphsIn {
|
||||||
}
|
}
|
||||||
|
|
||||||
message GraphsOut {
|
message GraphsOut {
|
||||||
enum Weekday {
|
|
||||||
SUNDAY = 0;
|
|
||||||
MONDAY = 1;
|
|
||||||
FRIDAY = 5;
|
|
||||||
SATURDAY = 6;
|
|
||||||
}
|
|
||||||
repeated Card cards = 1;
|
repeated Card cards = 1;
|
||||||
repeated RevlogEntry revlog = 2;
|
repeated RevlogEntry revlog = 2;
|
||||||
uint32 days_elapsed = 3;
|
uint32 days_elapsed = 3;
|
||||||
|
@ -1096,7 +1092,17 @@ message GraphsOut {
|
||||||
uint32 scheduler_version = 5;
|
uint32 scheduler_version = 5;
|
||||||
/// Seconds to add to UTC timestamps to get local time.
|
/// Seconds to add to UTC timestamps to get local time.
|
||||||
int32 local_offset_secs = 7;
|
int32 local_offset_secs = 7;
|
||||||
Weekday first_weekday = 8;
|
}
|
||||||
|
|
||||||
|
message GraphPreferences {
|
||||||
|
enum Weekday {
|
||||||
|
SUNDAY = 0;
|
||||||
|
MONDAY = 1;
|
||||||
|
FRIDAY = 5;
|
||||||
|
SATURDAY = 6;
|
||||||
|
}
|
||||||
|
Weekday calendar_first_day_of_week = 1;
|
||||||
|
bool card_counts_separate_inactive = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RevlogEntry {
|
message RevlogEntry {
|
||||||
|
|
|
@ -676,6 +676,15 @@ impl BackendService for Backend {
|
||||||
self.with_col(|col| col.graph_data_for_search(&input.search, input.days))
|
self.with_col(|col| col.graph_data_for_search(&input.search, input.days))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_graph_preferences(&self, _input: pb::Empty) -> BackendResult<pb::GraphPreferences> {
|
||||||
|
self.with_col(|col| col.get_graph_preferences())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_graph_preferences(&self, input: pb::GraphPreferences) -> BackendResult<Empty> {
|
||||||
|
self.with_col(|col| col.set_graph_preferences(input))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
// decks
|
// decks
|
||||||
//-----------------------------------------------
|
//-----------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,8 @@ pub(crate) enum ConfigKey {
|
||||||
ShowRemainingDueCountsInStudy,
|
ShowRemainingDueCountsInStudy,
|
||||||
ShowIntervalsAboveAnswerButtons,
|
ShowIntervalsAboveAnswerButtons,
|
||||||
NewReviewMix,
|
NewReviewMix,
|
||||||
FirstWeekday,
|
FirstDayOfWeek,
|
||||||
|
CardCountsSeparateInactive,
|
||||||
AnswerTimeLimitSecs,
|
AnswerTimeLimitSecs,
|
||||||
ShowDayLearningCardsFirst,
|
ShowDayLearningCardsFirst,
|
||||||
LastUnburiedDay,
|
LastUnburiedDay,
|
||||||
|
@ -76,7 +77,8 @@ impl From<ConfigKey> for &'static str {
|
||||||
ConfigKey::ShowRemainingDueCountsInStudy => "dueCounts",
|
ConfigKey::ShowRemainingDueCountsInStudy => "dueCounts",
|
||||||
ConfigKey::ShowIntervalsAboveAnswerButtons => "estTimes",
|
ConfigKey::ShowIntervalsAboveAnswerButtons => "estTimes",
|
||||||
ConfigKey::NewReviewMix => "newSpread",
|
ConfigKey::NewReviewMix => "newSpread",
|
||||||
ConfigKey::FirstWeekday => "firstWeekday",
|
ConfigKey::FirstDayOfWeek => "firstDayOfWeek",
|
||||||
|
ConfigKey::CardCountsSeparateInactive => "cardCountsSeparateInactive",
|
||||||
ConfigKey::AnswerTimeLimitSecs => "timeLim",
|
ConfigKey::AnswerTimeLimitSecs => "timeLim",
|
||||||
ConfigKey::ShowDayLearningCardsFirst => "dayLearnFirst",
|
ConfigKey::ShowDayLearningCardsFirst => "dayLearnFirst",
|
||||||
ConfigKey::LastUnburiedDay => "lastUnburied",
|
ConfigKey::LastUnburiedDay => "lastUnburied",
|
||||||
|
@ -229,11 +231,24 @@ impl Collection {
|
||||||
self.set_config(ConfigKey::NewReviewMix, &(mix as u8))
|
self.set_config(ConfigKey::NewReviewMix, &(mix as u8))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_first_weekday(&self) -> Weekday {
|
pub(crate) fn get_first_day_of_week(&self) -> Weekday {
|
||||||
self.get_config_optional(ConfigKey::FirstWeekday)
|
self.get_config_optional(ConfigKey::FirstDayOfWeek)
|
||||||
.unwrap_or(Weekday::Sunday)
|
.unwrap_or(Weekday::Sunday)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_first_day_of_week(&self, weekday: Weekday) -> Result<()> {
|
||||||
|
self.set_config(ConfigKey::FirstDayOfWeek, &weekday)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_card_counts_separate_inactive(&self) -> bool {
|
||||||
|
self.get_config_optional(ConfigKey::CardCountsSeparateInactive)
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_card_counts_separate_inactive(&self, separate: bool) -> Result<()> {
|
||||||
|
self.set_config(ConfigKey::CardCountsSeparateInactive, &separate)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_show_due_counts(&self) -> bool {
|
pub(crate) fn get_show_due_counts(&self) -> bool {
|
||||||
self.get_config_optional(ConfigKey::ShowRemainingDueCountsInStudy)
|
self.get_config_optional(ConfigKey::ShowRemainingDueCountsInStudy)
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// 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
|
||||||
|
|
||||||
use crate::{backend_proto as pb, prelude::*, revlog::RevlogEntry, search::SortMode};
|
use crate::{
|
||||||
|
backend_proto as pb, config::Weekday, prelude::*, revlog::RevlogEntry, search::SortMode,
|
||||||
|
};
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub(crate) fn graph_data_for_search(
|
pub(crate) fn graph_data_for_search(
|
||||||
|
@ -42,9 +44,26 @@ impl Collection {
|
||||||
next_day_at_secs: timing.next_day_at as u32,
|
next_day_at_secs: timing.next_day_at as u32,
|
||||||
scheduler_version: self.sched_ver() as u32,
|
scheduler_version: self.sched_ver() as u32,
|
||||||
local_offset_secs: local_offset_secs as i32,
|
local_offset_secs: local_offset_secs as i32,
|
||||||
first_weekday: self.get_first_weekday() as i32,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_graph_preferences(&self) -> Result<pb::GraphPreferences> {
|
||||||
|
Ok(pb::GraphPreferences {
|
||||||
|
calendar_first_day_of_week: self.get_first_day_of_week() as i32,
|
||||||
|
card_counts_separate_inactive: self.get_card_counts_separate_inactive(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_graph_preferences(&self, prefs: pb::GraphPreferences) -> Result<()> {
|
||||||
|
self.set_first_day_of_week(match prefs.calendar_first_day_of_week {
|
||||||
|
1 => Weekday::Monday,
|
||||||
|
5 => Weekday::Friday,
|
||||||
|
6 => Weekday::Saturday,
|
||||||
|
_ => Weekday::Sunday,
|
||||||
|
})?;
|
||||||
|
self.set_card_counts_separate_inactive(prefs.card_counts_separate_inactive)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RevlogEntry> for pb::RevlogEntry {
|
impl From<RevlogEntry> for pb::RevlogEntry {
|
||||||
|
|
|
@ -71,6 +71,7 @@ ts_library(
|
||||||
"@npm//d3-transition",
|
"@npm//d3-transition",
|
||||||
"@npm//lodash.debounce",
|
"@npm//lodash.debounce",
|
||||||
"@npm//lodash.throttle",
|
"@npm//lodash.throttle",
|
||||||
|
"@npm//svelte",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,18 @@
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
import { defaultGraphBounds, RevlogRange } from "./graph-helpers";
|
import { defaultGraphBounds, RevlogRange } from "./graph-helpers";
|
||||||
import { gatherData, renderCalendar } from "./calendar";
|
import { gatherData, renderCalendar } from "./calendar";
|
||||||
|
import type { PreferenceStore } from "./preferences";
|
||||||
import type { GraphData } from "./calendar";
|
import type { GraphData } from "./calendar";
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
export let preferences: PreferenceStore | null = null;
|
||||||
export let revlogRange: RevlogRange;
|
export let revlogRange: RevlogRange;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
export let nightMode: boolean;
|
export let nightMode: boolean;
|
||||||
|
|
||||||
|
let { calendarFirstDayOfWeek } = preferences;
|
||||||
let graphData: GraphData | null = null;
|
let graphData: GraphData | null = null;
|
||||||
|
|
||||||
let bounds = defaultGraphBounds();
|
let bounds = defaultGraphBounds();
|
||||||
|
@ -25,7 +28,7 @@
|
||||||
let targetYear = maxYear;
|
let targetYear = maxYear;
|
||||||
|
|
||||||
$: if (sourceData) {
|
$: if (sourceData) {
|
||||||
graphData = gatherData(sourceData);
|
graphData = gatherData(sourceData, $calendarFirstDayOfWeek);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
@ -49,7 +52,8 @@
|
||||||
targetYear,
|
targetYear,
|
||||||
i18n,
|
i18n,
|
||||||
nightMode,
|
nightMode,
|
||||||
revlogRange
|
revlogRange,
|
||||||
|
calendarFirstDayOfWeek.set
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,15 @@
|
||||||
import { defaultGraphBounds } from "./graph-helpers";
|
import { defaultGraphBounds } from "./graph-helpers";
|
||||||
import { gatherData, renderCards } from "./card-counts";
|
import { gatherData, renderCards } from "./card-counts";
|
||||||
import type { GraphData, TableDatum } from "./card-counts";
|
import type { GraphData, TableDatum } from "./card-counts";
|
||||||
|
import type { PreferenceStore } from "./preferences";
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
import SeparateInactiveCheckbox from "./SeparateInactiveCheckbox.svelte";
|
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut;
|
export let sourceData: pb.BackendProto.GraphsOut;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
export let preferences: PreferenceStore;
|
||||||
|
|
||||||
let separateInactive = true;
|
let { cardCountsSeparateInactive } = preferences;
|
||||||
let svg = null as HTMLElement | SVGElement | null;
|
let svg = null as HTMLElement | SVGElement | null;
|
||||||
|
|
||||||
let bounds = defaultGraphBounds();
|
let bounds = defaultGraphBounds();
|
||||||
|
@ -20,10 +21,11 @@
|
||||||
let tableData = (null as unknown) as TableDatum[];
|
let tableData = (null as unknown) as TableDatum[];
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
graphData = gatherData(sourceData, separateInactive, i18n);
|
graphData = gatherData(sourceData, $cardCountsSeparateInactive, i18n);
|
||||||
tableData = renderCards(svg as any, bounds, graphData);
|
tableData = renderCards(svg as any, bounds, graphData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const label = i18n.tr(i18n.TR.STATISTICS_COUNTS_SEPARATE_SUSPENDED_BURIED_CARDS);
|
||||||
const total = i18n.tr(i18n.TR.STATISTICS_COUNTS_TOTAL_CARDS);
|
const total = i18n.tr(i18n.TR.STATISTICS_COUNTS_TOTAL_CARDS);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -56,7 +58,10 @@
|
||||||
<h1>{graphData.title}</h1>
|
<h1>{graphData.title}</h1>
|
||||||
|
|
||||||
<div class="range-box-inner">
|
<div class="range-box-inner">
|
||||||
<SeparateInactiveCheckbox bind:separateInactive {i18n} />
|
<label>
|
||||||
|
<input type="checkbox" bind:checked={$cardCountsSeparateInactive} />
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="counts-outer">
|
<div class="counts-outer">
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type { SvelteComponent } from "svelte/internal";
|
import type { SvelteComponent } from "svelte/internal";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
import type { PreferenceStore } from "./preferences";
|
||||||
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 { getPreferences } from "./preferences";
|
||||||
|
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
export let nightMode: boolean;
|
export let nightMode: boolean;
|
||||||
|
@ -17,12 +19,18 @@
|
||||||
|
|
||||||
let active = false;
|
let active = false;
|
||||||
let sourceData: pb.BackendProto.GraphsOut | null = null;
|
let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
let preferences: PreferenceStore | null = null;
|
||||||
let revlogRange: RevlogRange;
|
let revlogRange: RevlogRange;
|
||||||
|
|
||||||
|
const preferencesPromise = getPreferences();
|
||||||
|
|
||||||
const refreshWith = async (search: string, days: number) => {
|
const refreshWith = async (search: string, days: number) => {
|
||||||
active = true;
|
active = true;
|
||||||
try {
|
try {
|
||||||
sourceData = await getGraphData(search, days);
|
[sourceData, preferences] = await Promise.all([
|
||||||
|
getGraphData(search, days),
|
||||||
|
preferencesPromise,
|
||||||
|
]);
|
||||||
revlogRange = days > 365 || days === 0 ? RevlogRange.All : RevlogRange.Year;
|
revlogRange = days > 365 || days === 0 ? RevlogRange.All : RevlogRange.Year;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sourceData = null;
|
sourceData = null;
|
||||||
|
@ -54,6 +62,7 @@
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={graph}
|
this={graph}
|
||||||
{sourceData}
|
{sourceData}
|
||||||
|
{preferences}
|
||||||
{revlogRange}
|
{revlogRange}
|
||||||
{i18n}
|
{i18n}
|
||||||
{nightMode} />
|
{nightMode} />
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<script lang="typescript">
|
|
||||||
import type { I18n } from "anki/i18n";
|
|
||||||
|
|
||||||
export let i18n: I18n;
|
|
||||||
export let separateInactive: boolean = true;
|
|
||||||
|
|
||||||
const label = i18n.tr(i18n.TR.STATISTICS_COUNTS_SEPARATE_SUSPENDED_BURIED_CARDS);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<label> <input type="checkbox" bind:checked={separateInactive} /> {label} </label>
|
|
|
@ -6,7 +6,7 @@
|
||||||
@typescript-eslint/no-explicit-any: "off",
|
@typescript-eslint/no-explicit-any: "off",
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type pb from "anki/backend_proto";
|
import pb from "anki/backend_proto";
|
||||||
import { interpolateBlues } from "d3-scale-chromatic";
|
import { interpolateBlues } from "d3-scale-chromatic";
|
||||||
import "d3-transition";
|
import "d3-transition";
|
||||||
import { select, mouse } from "d3-selection";
|
import { select, mouse } from "d3-selection";
|
||||||
|
@ -41,7 +41,13 @@ interface DayDatum {
|
||||||
date: Date;
|
date: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
type WeekdayType = pb.BackendProto.GraphPreferences.Weekday;
|
||||||
|
const Weekday = pb.BackendProto.GraphPreferences.Weekday; /* enum */
|
||||||
|
|
||||||
|
export function gatherData(
|
||||||
|
data: pb.BackendProto.GraphsOut,
|
||||||
|
firstDayOfWeek: WeekdayType
|
||||||
|
): GraphData {
|
||||||
const reviewCount = new Map<number, number>();
|
const reviewCount = new Map<number, number>();
|
||||||
|
|
||||||
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
|
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
|
||||||
|
@ -56,17 +62,17 @@ export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeFunction =
|
const timeFunction =
|
||||||
data.firstWeekday === 1
|
firstDayOfWeek === Weekday.MONDAY
|
||||||
? timeMonday
|
? timeMonday
|
||||||
: data.firstWeekday === 5
|
: firstDayOfWeek === Weekday.FRIDAY
|
||||||
? timeFriday
|
? timeFriday
|
||||||
: data.firstWeekday === 6
|
: firstDayOfWeek === Weekday.SATURDAY
|
||||||
? timeSaturday
|
? timeSaturday
|
||||||
: timeSunday;
|
: timeSunday;
|
||||||
|
|
||||||
const weekdayLabels: number[] = [];
|
const weekdayLabels: number[] = [];
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
weekdayLabels.push((data.firstWeekday + i) % 7);
|
weekdayLabels.push((firstDayOfWeek + i) % 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { reviewCount, timeFunction, weekdayLabels };
|
return { reviewCount, timeFunction, weekdayLabels };
|
||||||
|
@ -79,7 +85,8 @@ export function renderCalendar(
|
||||||
targetYear: number,
|
targetYear: number,
|
||||||
i18n: I18n,
|
i18n: I18n,
|
||||||
nightMode: boolean,
|
nightMode: boolean,
|
||||||
revlogRange: RevlogRange
|
revlogRange: RevlogRange,
|
||||||
|
setFirstDayOfWeek: (d: number) => void
|
||||||
): void {
|
): void {
|
||||||
const svg = select(svgElem);
|
const svg = select(svgElem);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -170,11 +177,19 @@ export function renderCalendar(
|
||||||
.attr("height", height - 2)
|
.attr("height", height - 2)
|
||||||
.attr("x", x(1)! - 3)
|
.attr("x", x(1)! - 3)
|
||||||
.attr("y", (_d, index) => bounds.marginTop + index * height)
|
.attr("y", (_d, index) => bounds.marginTop + index * height)
|
||||||
|
.attr("fill", nightMode ? "#ddd" : "black")
|
||||||
.attr("dominant-baseline", "hanging")
|
.attr("dominant-baseline", "hanging")
|
||||||
.attr("text-anchor", "end")
|
.attr("text-anchor", "end")
|
||||||
.attr("font-size", "small")
|
.attr("font-size", "small")
|
||||||
.attr("font-family", "monospace")
|
.attr("font-family", "monospace")
|
||||||
.style("user-select", "none");
|
.style("user-select", "none")
|
||||||
|
.on("click", null)
|
||||||
|
.filter((d: number) =>
|
||||||
|
[Weekday.SUNDAY, Weekday.MONDAY, Weekday.FRIDAY, Weekday.SATURDAY].includes(
|
||||||
|
d
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.on("click", setFirstDayOfWeek);
|
||||||
|
|
||||||
svg.select("g.days")
|
svg.select("g.days")
|
||||||
.selectAll("rect")
|
.selectAll("rect")
|
||||||
|
@ -192,11 +207,5 @@ export function renderCalendar(
|
||||||
.on("mouseout", hideTooltip)
|
.on("mouseout", hideTooltip)
|
||||||
.transition()
|
.transition()
|
||||||
.duration(800)
|
.duration(800)
|
||||||
.attr("fill", (d) => {
|
.attr("fill", (d) => (d.count === 0 ? emptyColour : blues(d.count)!));
|
||||||
if (d.count === 0) {
|
|
||||||
return emptyColour;
|
|
||||||
} else {
|
|
||||||
return blues(d.count)!;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import pb from "anki/backend_proto";
|
import pb from "anki/backend_proto";
|
||||||
import type { Selection } from "d3-selection";
|
import type { Selection } from "d3-selection";
|
||||||
|
import type { PreferencePayload } from "./preferences";
|
||||||
import { postRequest } from "anki/postrequest";
|
import { postRequest } from "anki/postrequest";
|
||||||
|
|
||||||
export async function getGraphData(
|
export async function getGraphData(
|
||||||
|
@ -19,6 +20,21 @@ export async function getGraphData(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getGraphPreferences(): Promise<pb.BackendProto.GraphPreferences> {
|
||||||
|
return pb.BackendProto.GraphPreferences.decode(
|
||||||
|
await postRequest("/_anki/graphPreferences", JSON.stringify({}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setGraphPreferences(prefs: PreferencePayload): Promise<void> {
|
||||||
|
return (async (): Promise<void> => {
|
||||||
|
await postRequest(
|
||||||
|
"/_anki/setGraphPreferences",
|
||||||
|
pb.BackendProto.GraphPreferences.encode(prefs).finish()
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
// amount of data to fetch from backend
|
// amount of data to fetch from backend
|
||||||
export enum RevlogRange {
|
export enum RevlogRange {
|
||||||
Year = 1,
|
Year = 1,
|
||||||
|
|
75
ts/graphs/preferences.ts
Normal file
75
ts/graphs/preferences.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import pb from "anki/backend_proto";
|
||||||
|
import { getGraphPreferences, setGraphPreferences } from "./graph-helpers";
|
||||||
|
import { Writable, writable, get } from "svelte/store";
|
||||||
|
|
||||||
|
export interface CustomStore<T> extends Writable<T> {
|
||||||
|
subscribe: (getter: (value: T) => void) => () => void;
|
||||||
|
set: (value: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PreferenceStore = {
|
||||||
|
[K in keyof Omit<pb.BackendProto.GraphPreferences, "toJSON">]: CustomStore<
|
||||||
|
pb.BackendProto.GraphPreferences[K]
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PreferencePayload = {
|
||||||
|
[K in keyof Omit<
|
||||||
|
pb.BackendProto.GraphPreferences,
|
||||||
|
"toJSON"
|
||||||
|
>]: pb.BackendProto.GraphPreferences[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
function createPreference<T>(
|
||||||
|
initialValue: T,
|
||||||
|
savePreferences: () => void
|
||||||
|
): CustomStore<T> {
|
||||||
|
const { subscribe, set, update } = writable(initialValue);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
set: (value: T): void => {
|
||||||
|
set(value);
|
||||||
|
savePreferences();
|
||||||
|
},
|
||||||
|
update: (updater: (value: T) => T): void => {
|
||||||
|
update(updater);
|
||||||
|
savePreferences();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function preparePreferences(
|
||||||
|
GraphPreferences: pb.BackendProto.GraphPreferences
|
||||||
|
): PreferenceStore {
|
||||||
|
const preferences: Partial<PreferenceStore> = {};
|
||||||
|
|
||||||
|
function constructPreferences(): PreferencePayload {
|
||||||
|
const payload: Partial<PreferencePayload> = {};
|
||||||
|
|
||||||
|
for (const key in preferences as PreferenceStore) {
|
||||||
|
payload[key] = get(preferences[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload as PreferencePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePreferences(): void {
|
||||||
|
setGraphPreferences(constructPreferences());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(
|
||||||
|
pb.BackendProto.GraphPreferences.toObject(GraphPreferences, {
|
||||||
|
defaults: true,
|
||||||
|
})
|
||||||
|
)) {
|
||||||
|
preferences[key] = createPreference(value, savePreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
return preferences as PreferenceStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPreferences(): Promise<PreferenceStore> {
|
||||||
|
const initialPreferences = await getGraphPreferences();
|
||||||
|
return preparePreferences(initialPreferences);
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"module": "es6",
|
"module": "es6",
|
||||||
"lib": ["es2016", "es2019.array", "dom", "dom.iterable"],
|
"lib": ["es2017", "es2019.array", "dom", "dom.iterable"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"anki/*": ["../bazel-bin/ts/lib/*"]
|
"anki/*": ["../bazel-bin/ts/lib/*"]
|
||||||
|
|
Loading…
Reference in a new issue