From 7f738c11a2fbf54d7a965d7eaa22f98cc8862f1d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 12 Apr 2021 14:18:30 +1000 Subject: [PATCH] deck config prototype work in progress Still in the early stages, and not hooked up yet. --- pylib/anki/decks.py | 4 + qt/aqt/data/web/pages/BUILD.bazel | 36 ++++----- qt/aqt/mediasrv.py | 8 ++ rslib/backend.proto | 26 ++++++ rslib/src/backend/deckconfig.rs | 8 ++ rslib/src/deckconf/mod.rs | 5 +- rslib/src/deckconf/update.rs | 82 +++++++++++++++++++ ts/deckconfig/BUILD.bazel | 116 +++++++++++++++++++++++++++ ts/deckconfig/CheckBox.svelte | 19 +++++ ts/deckconfig/ConfigEditor.svelte | 31 +++++++ ts/deckconfig/ConfigEntry.svelte | 36 +++++++++ ts/deckconfig/ConfigSelector.svelte | 58 ++++++++++++++ ts/deckconfig/DeckConfigPage.svelte | 49 +++++++++++ ts/deckconfig/EnumSelector.svelte | 21 +++++ ts/deckconfig/GeneralOptions.svelte | 43 ++++++++++ ts/deckconfig/LapseOptions.svelte | 58 ++++++++++++++ ts/deckconfig/NewOptions.svelte | 88 ++++++++++++++++++++ ts/deckconfig/OptionsDropdown.svelte | 30 +++++++ ts/deckconfig/RevertIcon.svelte | 33 ++++++++ ts/deckconfig/ReviewOptions.svelte | 66 +++++++++++++++ ts/deckconfig/SpinBox.svelte | 34 ++++++++ ts/deckconfig/SpinBoxFloat.svelte | 42 ++++++++++ ts/deckconfig/StepsInput.svelte | 45 +++++++++++ ts/deckconfig/deckconfig-base.scss | 15 ++++ ts/deckconfig/deckconfig.html | 19 +++++ ts/deckconfig/events.ts | 4 + ts/deckconfig/icons.ts | 7 ++ ts/deckconfig/index.ts | 23 ++++++ ts/deckconfig/lib.ts | 52 ++++++++++++ ts/deckconfig/steps.test.ts | 23 ++++++ ts/deckconfig/steps.ts | 70 ++++++++++++++++ ts/editor/BUILD.bazel | 9 ++- 32 files changed, 1133 insertions(+), 27 deletions(-) create mode 100644 rslib/src/deckconf/update.rs create mode 100644 ts/deckconfig/BUILD.bazel create mode 100644 ts/deckconfig/CheckBox.svelte create mode 100644 ts/deckconfig/ConfigEditor.svelte create mode 100644 ts/deckconfig/ConfigEntry.svelte create mode 100644 ts/deckconfig/ConfigSelector.svelte create mode 100644 ts/deckconfig/DeckConfigPage.svelte create mode 100644 ts/deckconfig/EnumSelector.svelte create mode 100644 ts/deckconfig/GeneralOptions.svelte create mode 100644 ts/deckconfig/LapseOptions.svelte create mode 100644 ts/deckconfig/NewOptions.svelte create mode 100644 ts/deckconfig/OptionsDropdown.svelte create mode 100644 ts/deckconfig/RevertIcon.svelte create mode 100644 ts/deckconfig/ReviewOptions.svelte create mode 100644 ts/deckconfig/SpinBox.svelte create mode 100644 ts/deckconfig/SpinBoxFloat.svelte create mode 100644 ts/deckconfig/StepsInput.svelte create mode 100644 ts/deckconfig/deckconfig-base.scss create mode 100644 ts/deckconfig/deckconfig.html create mode 100644 ts/deckconfig/events.ts create mode 100644 ts/deckconfig/icons.ts create mode 100644 ts/deckconfig/index.ts create mode 100644 ts/deckconfig/lib.ts create mode 100644 ts/deckconfig/steps.test.ts create mode 100644 ts/deckconfig/steps.ts diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 8bd047a54..022c3f27a 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -22,6 +22,7 @@ DeckTreeNode = _pb.DeckTreeNode DeckNameId = _pb.DeckNameId FilteredDeckConfig = _pb.Deck.Filtered DeckCollapseScope = _pb.SetDeckCollapsedIn.Scope +DeckConfigForUpdate = _pb.DeckConfigForUpdate # legacy code may pass this in as the type argument to .id() defaultDeck = 0 @@ -324,6 +325,9 @@ class DeckManager: # Deck configurations ############################################################# + def get_deck_config_for_update(self, deck_id: DeckId) -> DeckConfigForUpdate: + return self.col._backend.get_deck_config_for_update(deck_id) + def all_config(self) -> List[DeckConfigDict]: "A list of all deck config." return list(from_json_bytes(self.col._backend.all_deck_config_legacy())) diff --git a/qt/aqt/data/web/pages/BUILD.bazel b/qt/aqt/data/web/pages/BUILD.bazel index cf5066349..712025ad5 100644 --- a/qt/aqt/data/web/pages/BUILD.bazel +++ b/qt/aqt/data/web/pages/BUILD.bazel @@ -1,32 +1,24 @@ load("//ts:copy.bzl", "copy_files_into_group") -copy_files_into_group( - name = "graphs_page", - srcs = [ - "graphs-base.css", - "graphs.css", - "graphs.html", - "graphs.js", - ], - package = "//ts/graphs", -) +_pages = [ + "graphs", + "congrats", + "deckconfig", +] -copy_files_into_group( - name = "congrats_page", +[copy_files_into_group( + name = name + "_page", srcs = [ - "congrats-base.css", - "congrats.css", - "congrats.html", - "congrats.js", + name + "-base.css", + name + ".css", + name + ".html", + name + ".js", ], - package = "//ts/congrats", -) + package = "//ts/" + name, +) for name in _pages] filegroup( name = "pages", - srcs = [ - "congrats_page", - "graphs_page", - ], + srcs = [name + "_page" for name in _pages], visibility = ["//qt:__subpackages__"], ) diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index d39101cda..3985313a7 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -274,10 +274,18 @@ def i18n_resources() -> bytes: return aqt.mw.col.i18n_resources(modules=args["modules"]) +def deck_config_for_update() -> bytes: + args = from_json_bytes(request.data) + return aqt.mw.col.decks.get_deck_config_for_update( + deck_id=args["deckId"] + ).SerializeToString() + + post_handlers = { "graphData": graph_data, "graphPreferences": graph_preferences, "setGraphPreferences": set_graph_preferences, + "deckConfigForUpdate": deck_config_for_update, # pylint: disable=unnecessary-lambda "i18nResources": i18n_resources, "congratsInfo": congrats_info, diff --git a/rslib/backend.proto b/rslib/backend.proto index 46d45ba95..87ed71f1d 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -228,6 +228,8 @@ service DeckConfigService { rpc GetDeckConfigLegacy(DeckConfigId) returns (Json); rpc NewDeckConfigLegacy(Empty) returns (Json); rpc RemoveDeckConfig(DeckConfigId) returns (Empty); + rpc GetDeckConfigForUpdate(DeckId) returns (DeckConfigForUpdate); + rpc UpdateDeckConfig(UpdateDeckConfigIn) returns (OpChanges); } service TagsService { @@ -893,6 +895,30 @@ message AddOrUpdateDeckConfigLegacyIn { bool preserve_usn_and_mtime = 2; } +message DeckConfigForUpdate { + message ConfigWithExtra { + DeckConfig config = 1; + uint32 use_count = 2; + } + message CurrentDeck { + string name = 1; + int64 config_id = 2; + uint32 parent_new_limit = 3; + uint32 parent_review_limit = 4; + } + + repeated ConfigWithExtra all_config = 1; + CurrentDeck current_deck = 2; + DeckConfig defaults = 3; +} + +message UpdateDeckConfigIn { + int64 target_deck_id = 2; + DeckConfig desired_config = 3; + repeated int64 removed_config_ids = 4; + bool apply_to_children = 5; +} + message SetTagCollapsedIn { string name = 1; bool collapsed = 2; diff --git a/rslib/src/backend/deckconfig.rs b/rslib/src/backend/deckconfig.rs index 76bbd9133..53ce9c1a9 100644 --- a/rslib/src/backend/deckconfig.rs +++ b/rslib/src/backend/deckconfig.rs @@ -61,6 +61,14 @@ impl DeckConfigService for Backend { self.with_col(|col| col.transact_no_undo(|col| col.remove_deck_config(input.into()))) .map(Into::into) } + + fn get_deck_config_for_update(&self, input: pb::DeckId) -> Result { + self.with_col(|col| col.get_deck_config_for_update(input.into())) + } + + fn update_deck_config(&self, _input: pb::UpdateDeckConfigIn) -> Result { + todo!(); + } } impl From for pb::DeckConfig { diff --git a/rslib/src/deckconf/mod.rs b/rslib/src/deckconf/mod.rs index 117e84f90..d7d0675d5 100644 --- a/rslib/src/deckconf/mod.rs +++ b/rslib/src/deckconf/mod.rs @@ -1,6 +1,9 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +mod schema11; +mod update; + use crate::{ collection::Collection, define_newtype, @@ -18,8 +21,6 @@ pub use schema11::{DeckConfSchema11, NewCardOrderSchema11}; /// Old deck config and cards table store 250% as 2500. pub(crate) const INITIAL_EASE_FACTOR_THOUSANDS: u16 = (INITIAL_EASE_FACTOR * 1000.0) as u16; -mod schema11; - define_newtype!(DeckConfId, i64); #[derive(Debug, PartialEq, Clone)] diff --git a/rslib/src/deckconf/update.rs b/rslib/src/deckconf/update.rs new file mode 100644 index 000000000..739ffdf7c --- /dev/null +++ b/rslib/src/deckconf/update.rs @@ -0,0 +1,82 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::collections::{HashMap, HashSet}; + +use pb::deck_config_for_update::{ConfigWithExtra, CurrentDeck}; + +use crate::{backend_proto as pb, prelude::*}; + +impl Collection { + /// Information required for the deck options screen. + pub fn get_deck_config_for_update(&mut self, deck: DeckId) -> Result { + Ok(pb::DeckConfigForUpdate { + all_config: self.get_deck_config_with_extra_for_update()?, + current_deck: Some(self.get_current_deck_for_update(deck)?), + defaults: Some(DeckConf::default().into()), + }) + } +} + +impl Collection { + fn get_deck_config_with_extra_for_update(&self) -> Result> { + // grab the config and sort it + let mut config = self.storage.all_deck_config()?; + config.sort_unstable_by(|a, b| a.name.cmp(&b.name)); + + // combine with use counts + let counts = self.get_deck_config_use_counts()?; + Ok(config + .into_iter() + .map(|config| ConfigWithExtra { + use_count: counts.get(&config.id).cloned().unwrap_or_default() as u32, + config: Some(config.into()), + }) + .collect()) + } + + fn get_deck_config_use_counts(&self) -> Result> { + let mut counts = HashMap::new(); + for deck in self.storage.get_all_decks()? { + if let Ok(normal) = deck.normal() { + *counts.entry(DeckConfId(normal.config_id)).or_default() += 1; + } + } + + Ok(counts) + } + + fn get_current_deck_for_update(&mut self, deck: DeckId) -> Result { + let deck = self.get_deck(deck)?.ok_or(AnkiError::NotFound)?; + + let mut parent_new_limit = u32::MAX; + let mut parent_review_limit = u32::MAX; + for config_id in self.parent_config_ids(&deck)? { + if let Some(config) = self.storage.get_deck_config(config_id)? { + parent_new_limit = parent_new_limit.min(config.inner.new_per_day); + parent_review_limit = parent_review_limit.min(config.inner.reviews_per_day); + } + } + + Ok(CurrentDeck { + name: deck.name.clone(), + config_id: deck.normal()?.config_id, + parent_new_limit, + parent_review_limit, + }) + } + + /// Deck configs used by parent decks. + fn parent_config_ids(&self, deck: &Deck) -> Result> { + Ok(self + .storage + .parent_decks(deck)? + .iter() + .filter_map(|deck| { + deck.normal() + .ok() + .map(|normal| DeckConfId(normal.config_id)) + }) + .collect()) + } +} diff --git a/ts/deckconfig/BUILD.bazel b/ts/deckconfig/BUILD.bazel new file mode 100644 index 000000000..c1d29e812 --- /dev/null +++ b/ts/deckconfig/BUILD.bazel @@ -0,0 +1,116 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") +load("//ts:prettier.bzl", "prettier_test") +load("//ts:eslint.bzl", "eslint_test") +load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte", "svelte_check") +load("//ts:esbuild.bzl", "esbuild") +load("//ts:vendor.bzl", "copy_bootstrap_icons") +load("//ts:compile_sass.bzl", "compile_sass") + +compile_sass( + srcs = ["deckconfig-base.scss"], + group = "base_css", + visibility = ["//visibility:public"], + deps = [ + "//ts/bootstrap:scss", + "//ts/sass:base_lib", + "//ts/sass:scrollbar_lib", + ], +) + +svelte_files = glob(["*.svelte"]) + +svelte_names = [f.replace(".svelte", "") for f in svelte_files] + +compile_svelte( + name = "svelte", + srcs = svelte_files, +) + +copy_bootstrap_icons( + name = "bootstrap-icons", + icons = [ + "arrow-counterclockwise.svg", + ], +) + +ts_library( + name = "index", + srcs = ["index.ts"], + deps = [ + "DeckConfigPage", + "lib", + "//ts/lib", + "@npm//svelte2tsx", + ], +) + +ts_library( + name = "lib", + srcs = [ + "icons.ts", + "lib.ts", + "steps.ts", + ], + module_name = "deckconfig", + deps = [ + "//ts:image_module_support", + "//ts/lib", + "//ts/lib:backend_proto", + ], +) + +esbuild( + name = "deckconfig", + srcs = [ + "//ts:protobuf-shim.js", + ], + args = [ + "--global-name=anki", + "--inject:$(location //ts:protobuf-shim.js)", + "--resolve-extensions=.mjs,.js", + "--log-level=warning", + "--loader:.svg=text", + ], + entry_point = "index.ts", + external = [ + "protobufjs/light", + ], + output_css = True, + visibility = ["//visibility:public"], + deps = [ + "index", + "//ts/lib", + "//ts/lib:backend_proto", + ":bootstrap-icons", + "@npm//bootstrap", + ":base_css", + ] + svelte_names, +) + +exports_files(["deckconfig.html"]) + +# Tests +################ + +prettier_test( + name = "format_check", + srcs = glob([ + "*.ts", + "*.svelte", + ]), +) + +eslint_test( + name = "eslint", + srcs = glob([ + "*.ts", + ]), +) + +svelte_check( + name = "svelte_check", + srcs = glob([ + "*.ts", + "*.svelte", + ]), +) diff --git a/ts/deckconfig/CheckBox.svelte b/ts/deckconfig/CheckBox.svelte new file mode 100644 index 000000000..9cb0269b9 --- /dev/null +++ b/ts/deckconfig/CheckBox.svelte @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/ts/deckconfig/ConfigEditor.svelte b/ts/deckconfig/ConfigEditor.svelte new file mode 100644 index 000000000..a4ca85fa0 --- /dev/null +++ b/ts/deckconfig/ConfigEditor.svelte @@ -0,0 +1,31 @@ + + + + + +
+ + + + +
diff --git a/ts/deckconfig/ConfigEntry.svelte b/ts/deckconfig/ConfigEntry.svelte new file mode 100644 index 000000000..a6cbe75f0 --- /dev/null +++ b/ts/deckconfig/ConfigEntry.svelte @@ -0,0 +1,36 @@ + + + + + +
+ {label} + +
{subLabel}
+ +
+ +
+
diff --git a/ts/deckconfig/ConfigSelector.svelte b/ts/deckconfig/ConfigSelector.svelte new file mode 100644 index 000000000..52faf5219 --- /dev/null +++ b/ts/deckconfig/ConfigSelector.svelte @@ -0,0 +1,58 @@ + + + + + +
+
+ + + +
+
diff --git a/ts/deckconfig/DeckConfigPage.svelte b/ts/deckconfig/DeckConfigPage.svelte new file mode 100644 index 000000000..a6ae44078 --- /dev/null +++ b/ts/deckconfig/DeckConfigPage.svelte @@ -0,0 +1,49 @@ + + + + + +
+
+
{tr.actionsOptionsFor({ val: state.deckName })}
+ + + + +
+
diff --git a/ts/deckconfig/EnumSelector.svelte b/ts/deckconfig/EnumSelector.svelte new file mode 100644 index 000000000..aa26f5879 --- /dev/null +++ b/ts/deckconfig/EnumSelector.svelte @@ -0,0 +1,21 @@ + + + + + + diff --git a/ts/deckconfig/GeneralOptions.svelte b/ts/deckconfig/GeneralOptions.svelte new file mode 100644 index 000000000..f91e93348 --- /dev/null +++ b/ts/deckconfig/GeneralOptions.svelte @@ -0,0 +1,43 @@ + + + +
+

General

+ + + + + + + + +
diff --git a/ts/deckconfig/LapseOptions.svelte b/ts/deckconfig/LapseOptions.svelte new file mode 100644 index 000000000..0c30a49dd --- /dev/null +++ b/ts/deckconfig/LapseOptions.svelte @@ -0,0 +1,58 @@ + + + +
+

Lapses

+ + (config.relearnSteps = evt.detail.value)} /> + + (config.lapseMultiplier = evt.detail.value)} /> + + + + + + +
diff --git a/ts/deckconfig/NewOptions.svelte b/ts/deckconfig/NewOptions.svelte new file mode 100644 index 000000000..b77f02b2f --- /dev/null +++ b/ts/deckconfig/NewOptions.svelte @@ -0,0 +1,88 @@ + + + +
+

New Cards

+ + (config.learnSteps = evt.detail.value)} /> + + + + + + + + + + (config.easyMultiplier = evt.detail.value)} /> + + +
diff --git a/ts/deckconfig/OptionsDropdown.svelte b/ts/deckconfig/OptionsDropdown.svelte new file mode 100644 index 000000000..23d82d1b2 --- /dev/null +++ b/ts/deckconfig/OptionsDropdown.svelte @@ -0,0 +1,30 @@ + + + +
+ + + +
diff --git a/ts/deckconfig/RevertIcon.svelte b/ts/deckconfig/RevertIcon.svelte new file mode 100644 index 000000000..ca1e8dd2e --- /dev/null +++ b/ts/deckconfig/RevertIcon.svelte @@ -0,0 +1,33 @@ + + + + + +{#if modified} + {@html revertIcon} +{/if} diff --git a/ts/deckconfig/ReviewOptions.svelte b/ts/deckconfig/ReviewOptions.svelte new file mode 100644 index 000000000..c803ede3a --- /dev/null +++ b/ts/deckconfig/ReviewOptions.svelte @@ -0,0 +1,66 @@ + + + +
+

Reviews

+ + + + (config.easyMultiplier = evt.detail.value)} /> + + (config.intervalMultiplier = evt.detail.value)} /> + + + + (config.hardMultiplier = evt.detail.value)} /> + + +
diff --git a/ts/deckconfig/SpinBox.svelte b/ts/deckconfig/SpinBox.svelte new file mode 100644 index 000000000..0693771fd --- /dev/null +++ b/ts/deckconfig/SpinBox.svelte @@ -0,0 +1,34 @@ + + + + + + diff --git a/ts/deckconfig/SpinBoxFloat.svelte b/ts/deckconfig/SpinBoxFloat.svelte new file mode 100644 index 000000000..b7a550ef1 --- /dev/null +++ b/ts/deckconfig/SpinBoxFloat.svelte @@ -0,0 +1,42 @@ + + + + + + diff --git a/ts/deckconfig/StepsInput.svelte b/ts/deckconfig/StepsInput.svelte new file mode 100644 index 000000000..2cc257bfb --- /dev/null +++ b/ts/deckconfig/StepsInput.svelte @@ -0,0 +1,45 @@ + + + + + + + + diff --git a/ts/deckconfig/deckconfig-base.scss b/ts/deckconfig/deckconfig-base.scss new file mode 100644 index 000000000..9f4cf400a --- /dev/null +++ b/ts/deckconfig/deckconfig-base.scss @@ -0,0 +1,15 @@ +@use "ts/sass/scrollbar"; +@use "ts/sass/core"; + +@import "ts/bootstrap/functions"; +@import "ts/bootstrap/variables"; +@import "ts/bootstrap/mixins"; +@import "ts/bootstrap/helpers"; +@import "ts/bootstrap/dropdown"; +@import "ts/bootstrap/forms"; +@import "ts/bootstrap/buttons"; +@import "ts/bootstrap/button-group"; + +.night-mode { + @include scrollbar.night-mode; +} diff --git a/ts/deckconfig/deckconfig.html b/ts/deckconfig/deckconfig.html new file mode 100644 index 000000000..95957efd6 --- /dev/null +++ b/ts/deckconfig/deckconfig.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + +
+ + + + diff --git a/ts/deckconfig/events.ts b/ts/deckconfig/events.ts new file mode 100644 index 000000000..974333a32 --- /dev/null +++ b/ts/deckconfig/events.ts @@ -0,0 +1,4 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +export type NumberValueEvent = { detail: { value: number } }; diff --git a/ts/deckconfig/icons.ts b/ts/deckconfig/icons.ts new file mode 100644 index 000000000..e84bafcb6 --- /dev/null +++ b/ts/deckconfig/icons.ts @@ -0,0 +1,7 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +// Import icons from bootstrap + +import revertIcon from "./arrow-counterclockwise.svg"; +export { revertIcon }; diff --git a/ts/deckconfig/index.ts b/ts/deckconfig/index.ts new file mode 100644 index 000000000..015fd6788 --- /dev/null +++ b/ts/deckconfig/index.ts @@ -0,0 +1,23 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import { getDeckConfigInfo, stateFromUpdateData } from "./lib"; +import { setupI18n, ModuleName } from "anki/i18n"; +import { checkNightMode } from "anki/nightmode"; +import DeckConfigPage from "./DeckConfigPage.svelte"; + +export async function deckConfig( + target: HTMLDivElement, + deckId: number +): Promise { + checkNightMode(); + await setupI18n({ + modules: [ModuleName.SCHEDULING, ModuleName.ACTIONS, ModuleName.DECK_CONFIG], + }); + const info = await getDeckConfigInfo(deckId); + const state = stateFromUpdateData(info); + new DeckConfigPage({ + target, + props: { state }, + }); +} diff --git a/ts/deckconfig/lib.ts b/ts/deckconfig/lib.ts new file mode 100644 index 000000000..39501529a --- /dev/null +++ b/ts/deckconfig/lib.ts @@ -0,0 +1,52 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +/* eslint +@typescript-eslint/no-non-null-assertion: "off", + */ + +import pb from "anki/backend_proto"; +import { postRequest } from "anki/postrequest"; + +export async function getDeckConfigInfo( + deckId: number +): Promise { + return pb.BackendProto.DeckConfigForUpdate.decode( + await postRequest("/_anki/deckConfigForUpdate", JSON.stringify({ deckId })) + ); +} + +export type DeckConfigId = number; + +export interface ConfigWithCount { + config: pb.BackendProto.DeckConfig; + useCount: number; +} + +export interface DeckConfigState { + deckName: string; + selectedConfigId: DeckConfigId; + removedConfigs: DeckConfigId[]; + renamedConfigs: Map; + allConfigs: ConfigWithCount[]; + defaults: pb.BackendProto.DeckConfig.Config; +} + +export function stateFromUpdateData( + data: pb.BackendProto.DeckConfigForUpdate +): DeckConfigState { + const current = data.currentDeck as pb.BackendProto.DeckConfigForUpdate.CurrentDeck; + return { + deckName: current.name, + selectedConfigId: current.configId, + removedConfigs: [], + renamedConfigs: new Map(), + allConfigs: data.allConfig.map((config) => { + return { + config: config.config as pb.BackendProto.DeckConfig, + useCount: config.useCount!, + }; + }), + defaults: data.defaults!.config! as pb.BackendProto.DeckConfig.Config, + }; +} diff --git a/ts/deckconfig/steps.test.ts b/ts/deckconfig/steps.test.ts new file mode 100644 index 000000000..9bc33eaba --- /dev/null +++ b/ts/deckconfig/steps.test.ts @@ -0,0 +1,23 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import { stepsToString, stringToSteps } from "./steps"; + +test("whole steps", () => { + const steps = [1, 10, 60, 120, 1440]; + const string = "1m 10m 1h 2h 1d"; + expect(stepsToString(steps)).toBe(string); + expect(stringToSteps(string)).toStrictEqual(steps); +}); +test("fractional steps", () => { + const steps = [1 / 60, 5 / 60, 1.5, 400]; + const string = "1s 5s 90s 400m"; + expect(stepsToString(steps)).toBe(string); + expect(stringToSteps(string)).toStrictEqual(steps); +}); + +test("parsing", () => { + expect(stringToSteps("")).toStrictEqual([]); + expect(stringToSteps(" ")).toStrictEqual([]); + expect(stringToSteps("1 hello 2")).toStrictEqual([1, 2]); +}); diff --git a/ts/deckconfig/steps.ts b/ts/deckconfig/steps.ts new file mode 100644 index 000000000..35c688602 --- /dev/null +++ b/ts/deckconfig/steps.ts @@ -0,0 +1,70 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import { TimespanUnit, naturalWholeUnit, unitAmount, unitSeconds } from "anki/time"; + +function unitSuffix(unit: TimespanUnit): string { + switch (unit) { + case TimespanUnit.Seconds: + return "s"; + case TimespanUnit.Minutes: + return "m"; + case TimespanUnit.Hours: + return "h"; + case TimespanUnit.Days: + return "d"; + default: + // should not happen + return ""; + } +} + +function suffixToUnit(suffix: string): TimespanUnit { + switch (suffix) { + case "s": + return TimespanUnit.Seconds; + case "h": + return TimespanUnit.Hours; + case "d": + return TimespanUnit.Days; + default: + return TimespanUnit.Minutes; + } +} + +function minutesToString(step: number): string { + const secs = step * 60; + let unit = naturalWholeUnit(secs); + if ([TimespanUnit.Months, TimespanUnit.Years].includes(unit)) { + unit = TimespanUnit.Days; + } + const amount = unitAmount(unit, secs); + + return `${amount}${unitSuffix(unit)}`; +} + +function stringToMinutes(text: string): number { + const match = text.match(/(\d+)(.*)/); + if (match) { + const [_, num, suffix] = match; + const unit = suffixToUnit(suffix); + const seconds = unitSeconds(unit) * parseInt(num, 10); + return seconds / 60; + } else { + return 0; + } +} + +export function stepsToString(steps: number[]): string { + return steps.map(minutesToString).join(" "); +} + +export function stringToSteps(text: string): number[] { + return ( + text + .split(" ") + .map(stringToMinutes) + // remove zeros + .filter((e) => e) + ); +} diff --git a/ts/editor/BUILD.bazel b/ts/editor/BUILD.bazel index 9b4253a8b..bdcf754fa 100644 --- a/ts/editor/BUILD.bazel +++ b/ts/editor/BUILD.bazel @@ -6,14 +6,17 @@ load("//ts:vendor.bzl", "copy_bootstrap_icons") load("//ts:compile_sass.bzl", "compile_sass") compile_sass( + srcs = [ + "editable.scss", + "editor.scss", + ], group = "base_css", - srcs = ["editor.scss", "editable.scss"], + visibility = ["//visibility:public"], deps = [ "//ts/sass:base_lib", "//ts/sass:buttons_lib", "//ts/sass:scrollbar_lib", ], - visibility = ["//visibility:public"], ) ts_library( @@ -41,9 +44,9 @@ esbuild( entry_point = "index_wrapper.ts", visibility = ["//visibility:public"], deps = [ + "base_css", ":bootstrap-icons", ":editor_ts", - "base_css", ], )