deck config prototype work in progress
Still in the early stages, and not hooked up yet.
This commit is contained in:
parent
e6306bb29d
commit
7f738c11a2
@ -22,6 +22,7 @@ DeckTreeNode = _pb.DeckTreeNode
|
|||||||
DeckNameId = _pb.DeckNameId
|
DeckNameId = _pb.DeckNameId
|
||||||
FilteredDeckConfig = _pb.Deck.Filtered
|
FilteredDeckConfig = _pb.Deck.Filtered
|
||||||
DeckCollapseScope = _pb.SetDeckCollapsedIn.Scope
|
DeckCollapseScope = _pb.SetDeckCollapsedIn.Scope
|
||||||
|
DeckConfigForUpdate = _pb.DeckConfigForUpdate
|
||||||
|
|
||||||
# legacy code may pass this in as the type argument to .id()
|
# legacy code may pass this in as the type argument to .id()
|
||||||
defaultDeck = 0
|
defaultDeck = 0
|
||||||
@ -324,6 +325,9 @@ class DeckManager:
|
|||||||
# Deck configurations
|
# 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]:
|
def all_config(self) -> List[DeckConfigDict]:
|
||||||
"A list of all deck config."
|
"A list of all deck config."
|
||||||
return list(from_json_bytes(self.col._backend.all_deck_config_legacy()))
|
return list(from_json_bytes(self.col._backend.all_deck_config_legacy()))
|
||||||
|
@ -1,32 +1,24 @@
|
|||||||
load("//ts:copy.bzl", "copy_files_into_group")
|
load("//ts:copy.bzl", "copy_files_into_group")
|
||||||
|
|
||||||
copy_files_into_group(
|
_pages = [
|
||||||
name = "graphs_page",
|
"graphs",
|
||||||
srcs = [
|
"congrats",
|
||||||
"graphs-base.css",
|
"deckconfig",
|
||||||
"graphs.css",
|
]
|
||||||
"graphs.html",
|
|
||||||
"graphs.js",
|
|
||||||
],
|
|
||||||
package = "//ts/graphs",
|
|
||||||
)
|
|
||||||
|
|
||||||
copy_files_into_group(
|
[copy_files_into_group(
|
||||||
name = "congrats_page",
|
name = name + "_page",
|
||||||
srcs = [
|
srcs = [
|
||||||
"congrats-base.css",
|
name + "-base.css",
|
||||||
"congrats.css",
|
name + ".css",
|
||||||
"congrats.html",
|
name + ".html",
|
||||||
"congrats.js",
|
name + ".js",
|
||||||
],
|
],
|
||||||
package = "//ts/congrats",
|
package = "//ts/" + name,
|
||||||
)
|
) for name in _pages]
|
||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "pages",
|
name = "pages",
|
||||||
srcs = [
|
srcs = [name + "_page" for name in _pages],
|
||||||
"congrats_page",
|
|
||||||
"graphs_page",
|
|
||||||
],
|
|
||||||
visibility = ["//qt:__subpackages__"],
|
visibility = ["//qt:__subpackages__"],
|
||||||
)
|
)
|
||||||
|
@ -274,10 +274,18 @@ def i18n_resources() -> bytes:
|
|||||||
return aqt.mw.col.i18n_resources(modules=args["modules"])
|
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 = {
|
post_handlers = {
|
||||||
"graphData": graph_data,
|
"graphData": graph_data,
|
||||||
"graphPreferences": graph_preferences,
|
"graphPreferences": graph_preferences,
|
||||||
"setGraphPreferences": set_graph_preferences,
|
"setGraphPreferences": set_graph_preferences,
|
||||||
|
"deckConfigForUpdate": deck_config_for_update,
|
||||||
# pylint: disable=unnecessary-lambda
|
# pylint: disable=unnecessary-lambda
|
||||||
"i18nResources": i18n_resources,
|
"i18nResources": i18n_resources,
|
||||||
"congratsInfo": congrats_info,
|
"congratsInfo": congrats_info,
|
||||||
|
@ -228,6 +228,8 @@ service DeckConfigService {
|
|||||||
rpc GetDeckConfigLegacy(DeckConfigId) returns (Json);
|
rpc GetDeckConfigLegacy(DeckConfigId) returns (Json);
|
||||||
rpc NewDeckConfigLegacy(Empty) returns (Json);
|
rpc NewDeckConfigLegacy(Empty) returns (Json);
|
||||||
rpc RemoveDeckConfig(DeckConfigId) returns (Empty);
|
rpc RemoveDeckConfig(DeckConfigId) returns (Empty);
|
||||||
|
rpc GetDeckConfigForUpdate(DeckId) returns (DeckConfigForUpdate);
|
||||||
|
rpc UpdateDeckConfig(UpdateDeckConfigIn) returns (OpChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
service TagsService {
|
service TagsService {
|
||||||
@ -893,6 +895,30 @@ message AddOrUpdateDeckConfigLegacyIn {
|
|||||||
bool preserve_usn_and_mtime = 2;
|
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 {
|
message SetTagCollapsedIn {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
bool collapsed = 2;
|
bool collapsed = 2;
|
||||||
|
@ -61,6 +61,14 @@ impl DeckConfigService for Backend {
|
|||||||
self.with_col(|col| col.transact_no_undo(|col| col.remove_deck_config(input.into())))
|
self.with_col(|col| col.transact_no_undo(|col| col.remove_deck_config(input.into())))
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_deck_config_for_update(&self, input: pb::DeckId) -> Result<pb::DeckConfigForUpdate> {
|
||||||
|
self.with_col(|col| col.get_deck_config_for_update(input.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_deck_config(&self, _input: pb::UpdateDeckConfigIn) -> Result<pb::OpChanges> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeckConf> for pb::DeckConfig {
|
impl From<DeckConf> for pb::DeckConfig {
|
||||||
|
@ -1,6 +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
|
||||||
|
|
||||||
|
mod schema11;
|
||||||
|
mod update;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
define_newtype,
|
define_newtype,
|
||||||
@ -18,8 +21,6 @@ pub use schema11::{DeckConfSchema11, NewCardOrderSchema11};
|
|||||||
/// Old deck config and cards table store 250% as 2500.
|
/// 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;
|
pub(crate) const INITIAL_EASE_FACTOR_THOUSANDS: u16 = (INITIAL_EASE_FACTOR * 1000.0) as u16;
|
||||||
|
|
||||||
mod schema11;
|
|
||||||
|
|
||||||
define_newtype!(DeckConfId, i64);
|
define_newtype!(DeckConfId, i64);
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
82
rslib/src/deckconf/update.rs
Normal file
82
rslib/src/deckconf/update.rs
Normal file
@ -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<pb::DeckConfigForUpdate> {
|
||||||
|
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<Vec<ConfigWithExtra>> {
|
||||||
|
// 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<HashMap<DeckConfId, usize>> {
|
||||||
|
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<CurrentDeck> {
|
||||||
|
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<HashSet<DeckConfId>> {
|
||||||
|
Ok(self
|
||||||
|
.storage
|
||||||
|
.parent_decks(deck)?
|
||||||
|
.iter()
|
||||||
|
.filter_map(|deck| {
|
||||||
|
deck.normal()
|
||||||
|
.ok()
|
||||||
|
.map(|normal| DeckConfId(normal.config_id))
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
116
ts/deckconfig/BUILD.bazel
Normal file
116
ts/deckconfig/BUILD.bazel
Normal file
@ -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",
|
||||||
|
]),
|
||||||
|
)
|
19
ts/deckconfig/CheckBox.svelte
Normal file
19
ts/deckconfig/CheckBox.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import ConfigEntry from "./ConfigEntry.svelte";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let subLabel: string;
|
||||||
|
export let value: boolean;
|
||||||
|
export let defaultValue: boolean;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<ConfigEntry {label} bind:value {defaultValue}>
|
||||||
|
<label> <input type="checkbox" bind:checked={value} /> {subLabel} </label>
|
||||||
|
</ConfigEntry>
|
31
ts/deckconfig/ConfigEditor.svelte
Normal file
31
ts/deckconfig/ConfigEditor.svelte
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import NewOptions from "./NewOptions.svelte";
|
||||||
|
import ReviewOptions from "./ReviewOptions.svelte";
|
||||||
|
import LapseOptions from "./LapseOptions.svelte";
|
||||||
|
import GeneralOptions from "./GeneralOptions.svelte";
|
||||||
|
|
||||||
|
export let config: pb.BackendProto.DeckConfig.Config;
|
||||||
|
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
:global(h2) {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.warn) {
|
||||||
|
background-color: var(--flag1-bg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<NewOptions bind:config {defaults} />
|
||||||
|
<ReviewOptions bind:config {defaults} />
|
||||||
|
<LapseOptions bind:config {defaults} />
|
||||||
|
<GeneralOptions bind:config {defaults} />
|
||||||
|
</div>
|
36
ts/deckconfig/ConfigEntry.svelte
Normal file
36
ts/deckconfig/ConfigEntry.svelte
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import RevertIcon from "./RevertIcon.svelte";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let subLabel = "";
|
||||||
|
export let value: any;
|
||||||
|
export let defaultValue: any;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.outer {
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin-top: 0.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="outer">
|
||||||
|
<span class="label">{label}</span>
|
||||||
|
<RevertIcon bind:value {defaultValue} on:revert />
|
||||||
|
<div class="sub">{subLabel}</div>
|
||||||
|
|
||||||
|
<div class="inner">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
58
ts/deckconfig/ConfigSelector.svelte
Normal file
58
ts/deckconfig/ConfigSelector.svelte
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
import type { DeckConfigId, ConfigWithCount } from "./lib";
|
||||||
|
import OptionsDropdown from "./OptionsDropdown.svelte";
|
||||||
|
|
||||||
|
export let allConfig: ConfigWithCount[];
|
||||||
|
export let selectedConfigId: DeckConfigId;
|
||||||
|
|
||||||
|
function configLabel(config: ConfigWithCount): string {
|
||||||
|
const name = config.config.name;
|
||||||
|
const count = tr.deckConfigUsedByDecks({ decks: config.useCount });
|
||||||
|
return `${name} (${count})`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.form-select {
|
||||||
|
display: inline-block;
|
||||||
|
width: 30em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outer {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--text-fg);
|
||||||
|
background: var(--window-bg);
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
& > :global(*) {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner">
|
||||||
|
<select bind:value={selectedConfigId} class="form-select">
|
||||||
|
{#each allConfig as config}
|
||||||
|
<option value={config.config.id}>{configLabel(config)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<OptionsDropdown />
|
||||||
|
</div>
|
||||||
|
</div>
|
49
ts/deckconfig/DeckConfigPage.svelte
Normal file
49
ts/deckconfig/DeckConfigPage.svelte
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import ConfigSelector from "./ConfigSelector.svelte";
|
||||||
|
import ConfigEditor from "./ConfigEditor.svelte";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
import type { DeckConfigState } from "./lib";
|
||||||
|
|
||||||
|
export let state: DeckConfigState;
|
||||||
|
|
||||||
|
let selectedConfigId = state.selectedConfigId;
|
||||||
|
|
||||||
|
let selectedConfig: pb.BackendProto.DeckConfig.Config;
|
||||||
|
$: {
|
||||||
|
selectedConfig = (
|
||||||
|
state.allConfigs.find((e) => e.config.id == selectedConfigId)?.config ??
|
||||||
|
state.allConfigs[0].config
|
||||||
|
).config as pb.BackendProto.DeckConfig.Config;
|
||||||
|
}
|
||||||
|
let defaults = state.defaults;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.outer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(input, select) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="outer">
|
||||||
|
<div class="inner">
|
||||||
|
<div><b>{tr.actionsOptionsFor({ val: state.deckName })}</b></div>
|
||||||
|
|
||||||
|
<ConfigSelector allConfig={state.allConfigs} bind:selectedConfigId />
|
||||||
|
|
||||||
|
<ConfigEditor config={selectedConfig} {defaults} />
|
||||||
|
</div>
|
||||||
|
</div>
|
21
ts/deckconfig/EnumSelector.svelte
Normal file
21
ts/deckconfig/EnumSelector.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import ConfigEntry from "./ConfigEntry.svelte";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let subLabel: string;
|
||||||
|
export let choices: string[];
|
||||||
|
export let value: number = 0;
|
||||||
|
export let defaultValue: number;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfigEntry {label} {subLabel} bind:value {defaultValue}>
|
||||||
|
<select bind:value class="form-select">
|
||||||
|
{#each choices as choice, idx}
|
||||||
|
<option value={idx}>{choice}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</ConfigEntry>
|
43
ts/deckconfig/GeneralOptions.svelte
Normal file
43
ts/deckconfig/GeneralOptions.svelte
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
import SpinBox from "./SpinBox.svelte";
|
||||||
|
import CheckBox from "./CheckBox.svelte";
|
||||||
|
|
||||||
|
export let config: pb.BackendProto.DeckConfig.Config;
|
||||||
|
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>General</h2>
|
||||||
|
|
||||||
|
<SpinBox
|
||||||
|
label={tr.schedulingIgnoreAnswerTimesLongerThan()}
|
||||||
|
subLabel="The maximum number of seconds to record for a single review."
|
||||||
|
min={30}
|
||||||
|
max={600}
|
||||||
|
defaultValue={defaults.capAnswerTimeToSecs}
|
||||||
|
bind:value={config.capAnswerTimeToSecs} />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
label="Answer timer"
|
||||||
|
subLabel={tr.schedulingShowAnswerTimer()}
|
||||||
|
defaultValue={defaults.showTimer}
|
||||||
|
bind:value={config.showTimer} />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
label="Autoplay"
|
||||||
|
subLabel="Don't play audio automatically"
|
||||||
|
defaultValue={defaults.disableAutoplay}
|
||||||
|
bind:value={config.disableAutoplay} />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
label="Question Audio"
|
||||||
|
subLabel={tr.schedulingAlwaysIncludeQuestionSideWhenReplaying()}
|
||||||
|
defaultValue={defaults.skipQuestionWhenReplayingAnswer}
|
||||||
|
bind:value={config.skipQuestionWhenReplayingAnswer} />
|
||||||
|
</div>
|
58
ts/deckconfig/LapseOptions.svelte
Normal file
58
ts/deckconfig/LapseOptions.svelte
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
import SpinBox from "./SpinBox.svelte";
|
||||||
|
import SpinBoxFloat from "./SpinBoxFloat.svelte";
|
||||||
|
import StepsInput from "./StepsInput.svelte";
|
||||||
|
import EnumSelector from "./EnumSelector.svelte";
|
||||||
|
|
||||||
|
export let config: pb.BackendProto.DeckConfig.Config;
|
||||||
|
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||||
|
|
||||||
|
const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Lapses</h2>
|
||||||
|
|
||||||
|
<StepsInput
|
||||||
|
label="Relearning steps"
|
||||||
|
subLabel="Relearning steps, separated by spaces."
|
||||||
|
defaultValue={defaults.relearnSteps}
|
||||||
|
value={config.relearnSteps}
|
||||||
|
on:changed={(evt) => (config.relearnSteps = evt.detail.value)} />
|
||||||
|
|
||||||
|
<SpinBoxFloat
|
||||||
|
label={tr.schedulingNewInterval()}
|
||||||
|
subLabel="The multiplier applied to review cards when answering Again."
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
defaultValue={defaults.lapseMultiplier}
|
||||||
|
value={config.lapseMultiplier}
|
||||||
|
on:changed={(evt) => (config.lapseMultiplier = evt.detail.value)} />
|
||||||
|
|
||||||
|
<SpinBox
|
||||||
|
label={tr.schedulingMinimumInterval()}
|
||||||
|
subLabel="The minimum new interval a lapsed card will be given after relearning."
|
||||||
|
min={1}
|
||||||
|
defaultValue={defaults.minimumLapseInterval}
|
||||||
|
bind:value={config.minimumLapseInterval} />
|
||||||
|
|
||||||
|
<SpinBox
|
||||||
|
label={tr.schedulingLeechThreshold()}
|
||||||
|
subLabel="Number of times Again needs to be pressed on a review card to make it a leech."
|
||||||
|
min={1}
|
||||||
|
defaultValue={defaults.leechThreshold}
|
||||||
|
bind:value={config.leechThreshold} />
|
||||||
|
|
||||||
|
<EnumSelector
|
||||||
|
label={tr.schedulingLeechAction()}
|
||||||
|
subLabel=""
|
||||||
|
choices={leechChoices}
|
||||||
|
defaultValue={defaults.leechAction}
|
||||||
|
bind:value={config.leechAction} />
|
||||||
|
</div>
|
88
ts/deckconfig/NewOptions.svelte
Normal file
88
ts/deckconfig/NewOptions.svelte
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
import SpinBox from "./SpinBox.svelte";
|
||||||
|
import SpinBoxFloat from "./SpinBoxFloat.svelte";
|
||||||
|
import CheckBox from "./CheckBox.svelte";
|
||||||
|
import StepsInput from "./StepsInput.svelte";
|
||||||
|
import EnumSelector from "./EnumSelector.svelte";
|
||||||
|
|
||||||
|
export let config: pb.BackendProto.DeckConfig.Config;
|
||||||
|
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||||
|
|
||||||
|
const newOrderChoices = [
|
||||||
|
tr.schedulingShowNewCardsInOrderAdded(),
|
||||||
|
tr.schedulingShowNewCardsInRandomOrder(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let stepsExceedGraduatingInterval: boolean;
|
||||||
|
$: {
|
||||||
|
const lastLearnStepInDays = config.learnSteps.length
|
||||||
|
? config.learnSteps[config.learnSteps.length - 1] / 60 / 24
|
||||||
|
: 0;
|
||||||
|
stepsExceedGraduatingInterval =
|
||||||
|
lastLearnStepInDays > config.graduatingIntervalGood;
|
||||||
|
}
|
||||||
|
|
||||||
|
let goodExceedsEasy: boolean;
|
||||||
|
$: goodExceedsEasy = config.graduatingIntervalGood > config.graduatingIntervalEasy;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>New Cards</h2>
|
||||||
|
|
||||||
|
<StepsInput
|
||||||
|
label="Learning steps"
|
||||||
|
subLabel="Learning steps, separated by spaces."
|
||||||
|
warn={stepsExceedGraduatingInterval}
|
||||||
|
defaultValue={defaults.learnSteps}
|
||||||
|
value={config.learnSteps}
|
||||||
|
on:changed={(evt) => (config.learnSteps = evt.detail.value)} />
|
||||||
|
|
||||||
|
<EnumSelector
|
||||||
|
label={tr.schedulingOrder()}
|
||||||
|
subLabel=""
|
||||||
|
choices={newOrderChoices}
|
||||||
|
defaultValue={defaults.newCardOrder}
|
||||||
|
bind:value={config.newCardOrder} />
|
||||||
|
|
||||||
|
<SpinBox
|
||||||
|
label={tr.schedulingNewCardsday()}
|
||||||
|
subLabel="The maximum number of new cards to introduce in a day."
|
||||||
|
min={0}
|
||||||
|
defaultValue={defaults.newPerDay}
|
||||||
|
bind:value={config.newPerDay} />
|
||||||
|
|
||||||
|
<SpinBox
|
||||||
|
label={tr.schedulingGraduatingInterval()}
|
||||||
|
subLabel="Days to wait after answering Good on the last learning step."
|
||||||
|
warn={stepsExceedGraduatingInterval || goodExceedsEasy}
|
||||||
|
defaultValue={defaults.graduatingIntervalGood}
|
||||||
|
bind:value={config.graduatingIntervalGood} />
|
||||||
|
|
||||||
|
<SpinBox
|
||||||
|
label={tr.schedulingEasyInterval()}
|
||||||
|
subLabel="Days to wait after answering Easy on the first learning step."
|
||||||
|
warn={goodExceedsEasy}
|
||||||
|
defaultValue={defaults.graduatingIntervalEasy}
|
||||||
|
bind:value={config.graduatingIntervalEasy} />
|
||||||
|
|
||||||
|
<SpinBoxFloat
|
||||||
|
label={tr.schedulingStartingEase()}
|
||||||
|
subLabel="The default multiplier when a review is answered Good."
|
||||||
|
min={1.31}
|
||||||
|
max={5}
|
||||||
|
defaultValue={defaults.initialEase}
|
||||||
|
value={config.initialEase}
|
||||||
|
on:changed={(evt) => (config.easyMultiplier = evt.detail.value)} />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
label="Bury New"
|
||||||
|
subLabel={tr.schedulingBuryRelatedNewCardsUntilThe()}
|
||||||
|
defaultValue={defaults.buryNew}
|
||||||
|
bind:value={config.buryNew} />
|
||||||
|
</div>
|
30
ts/deckconfig/OptionsDropdown.svelte
Normal file
30
ts/deckconfig/OptionsDropdown.svelte
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<style>
|
||||||
|
:global(svg) {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary">Save</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span class="visually-hidden">Toggle Dropdown</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href={'#'}>Add</a></li>
|
||||||
|
<li><a class="dropdown-item" href={'#'}>Rename</a></li>
|
||||||
|
<li><a class="dropdown-item" href={'#'}>Remove</a></li>
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
<input type="checkbox" class="form-check-input" id="dropdownCheck" />
|
||||||
|
<label class="form-check-label" for="dropdownCheck"> Apply to Children </label>
|
||||||
|
</ul>
|
||||||
|
</div>
|
33
ts/deckconfig/RevertIcon.svelte
Normal file
33
ts/deckconfig/RevertIcon.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { revertIcon } from "./icons";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let value: any;
|
||||||
|
export let defaultValue: any;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let modified: boolean;
|
||||||
|
$: modified = JSON.stringify(value) !== JSON.stringify(defaultValue);
|
||||||
|
|
||||||
|
/// This component can be used either with bind:value, or by listening
|
||||||
|
/// to the revert event.
|
||||||
|
function revert(): void {
|
||||||
|
value = JSON.parse(JSON.stringify(defaultValue));
|
||||||
|
dispatch("revert", { value });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(svg) {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#if modified}
|
||||||
|
<span on:click={revert}>{@html revertIcon}</span>
|
||||||
|
{/if}
|
66
ts/deckconfig/ReviewOptions.svelte
Normal file
66
ts/deckconfig/ReviewOptions.svelte
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
import SpinBox from "./SpinBox.svelte";
|
||||||
|
import SpinBoxFloat from "./SpinBoxFloat.svelte";
|
||||||
|
import CheckBox from "./CheckBox.svelte";
|
||||||
|
|
||||||
|
export let config: pb.BackendProto.DeckConfig.Config;
|
||||||
|
export let defaults: pb.BackendProto.DeckConfig.Config;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Reviews</h2>
|
||||||
|
|
||||||
|
<SpinBox
|
||||||
|
label={tr.schedulingMaximumReviewsday()}
|
||||||
|
subLabel="The maximum number of reviews cards to show in a day."
|
||||||
|
min={0}
|
||||||
|
defaultValue={defaults.reviewsPerDay}
|
||||||
|
bind:value={config.reviewsPerDay} />
|
||||||
|
|
||||||
|
<SpinBoxFloat
|
||||||
|
label={tr.schedulingEasyBonus()}
|
||||||
|
subLabel="Extra multiplier applied when answering Easy on a review card."
|
||||||
|
min={1}
|
||||||
|
max={3}
|
||||||
|
defaultValue={defaults.easyMultiplier}
|
||||||
|
value={config.easyMultiplier}
|
||||||
|
on:changed={(evt) => (config.easyMultiplier = evt.detail.value)} />
|
||||||
|
|
||||||
|
<SpinBoxFloat
|
||||||
|
label={tr.schedulingIntervalModifier()}
|
||||||
|
subLabel="Multiplier applied to all reviews."
|
||||||
|
min={0.5}
|
||||||
|
max={2}
|
||||||
|
defaultValue={defaults.intervalMultiplier}
|
||||||
|
value={config.intervalMultiplier}
|
||||||
|
on:changed={(evt) => (config.intervalMultiplier = evt.detail.value)} />
|
||||||
|
|
||||||
|
<SpinBox
|
||||||
|
label={tr.schedulingMaximumInterval()}
|
||||||
|
subLabel="The longest number of days a review card will wait."
|
||||||
|
min={1}
|
||||||
|
max={365 * 100}
|
||||||
|
defaultValue={defaults.maximumReviewInterval}
|
||||||
|
bind:value={config.maximumReviewInterval} />
|
||||||
|
|
||||||
|
<SpinBoxFloat
|
||||||
|
label={tr.schedulingHardInterval()}
|
||||||
|
subLabel="Multiplier applied to review interval when Hard is pressed."
|
||||||
|
min={0.5}
|
||||||
|
max={1.3}
|
||||||
|
defaultValue={defaults.hardMultiplier}
|
||||||
|
value={config.hardMultiplier}
|
||||||
|
on:changed={(evt) => (config.hardMultiplier = evt.detail.value)} />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
label="Bury Reviews"
|
||||||
|
subLabel={tr.schedulingBuryRelatedReviewsUntilTheNext()}
|
||||||
|
defaultValue={defaults.buryReviews}
|
||||||
|
bind:value={config.buryReviews} />
|
||||||
|
</div>
|
34
ts/deckconfig/SpinBox.svelte
Normal file
34
ts/deckconfig/SpinBox.svelte
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import ConfigEntry from "./ConfigEntry.svelte";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let subLabel: string;
|
||||||
|
export let value: number;
|
||||||
|
export let min = 1;
|
||||||
|
export let max = 9999;
|
||||||
|
export let warn = false;
|
||||||
|
export let defaultValue: number = 0;
|
||||||
|
|
||||||
|
function blur() {
|
||||||
|
if (value > max) {
|
||||||
|
value = max;
|
||||||
|
} else if (value < min) {
|
||||||
|
value = min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfigEntry {label} {subLabel} bind:value {defaultValue}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
{min}
|
||||||
|
{max}
|
||||||
|
bind:value
|
||||||
|
on:blur={blur}
|
||||||
|
class:warn
|
||||||
|
class="form-control" />
|
||||||
|
</ConfigEntry>
|
42
ts/deckconfig/SpinBoxFloat.svelte
Normal file
42
ts/deckconfig/SpinBoxFloat.svelte
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import ConfigEntry from "./ConfigEntry.svelte";
|
||||||
|
import type { NumberValueEvent } from "./events";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let subLabel: string;
|
||||||
|
export let value: number;
|
||||||
|
export let defaultValue: number;
|
||||||
|
export let min = 1;
|
||||||
|
export let max = 9999;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let stringValue: string;
|
||||||
|
$: stringValue = value.toFixed(2);
|
||||||
|
|
||||||
|
function update(this: HTMLInputElement): void {
|
||||||
|
dispatch("changed", {
|
||||||
|
value: Math.min(max, Math.max(min, parseFloat(this.value))),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function revert(evt: NumberValueEvent): void {
|
||||||
|
dispatch("changed", { value: evt.detail.value });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfigEntry {label} {subLabel} {value} {defaultValue} on:revert={revert}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
{min}
|
||||||
|
{max}
|
||||||
|
step="0.01"
|
||||||
|
value={stringValue}
|
||||||
|
on:blur={update}
|
||||||
|
class="form-control" />
|
||||||
|
</ConfigEntry>
|
45
ts/deckconfig/StepsInput.svelte
Normal file
45
ts/deckconfig/StepsInput.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { stepsToString, stringToSteps } from "./steps";
|
||||||
|
import ConfigEntry from "./ConfigEntry.svelte";
|
||||||
|
import type { NumberValueEvent } from "./events";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let subLabel: string;
|
||||||
|
export let value: number[];
|
||||||
|
export let defaultValue: number[];
|
||||||
|
export let warn: boolean = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let stringValue: string;
|
||||||
|
$: stringValue = stepsToString(value);
|
||||||
|
|
||||||
|
function update(this: HTMLInputElement): void {
|
||||||
|
const value = stringToSteps(this.value);
|
||||||
|
dispatch("changed", { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function revert(evt: NumberValueEvent): void {
|
||||||
|
dispatch("changed", { value: evt.detail.value });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<ConfigEntry {label} {subLabel} {value} {defaultValue} on:revert={revert}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={stringValue}
|
||||||
|
on:blur={update}
|
||||||
|
class:warn
|
||||||
|
class="form-control" />
|
||||||
|
</ConfigEntry>
|
15
ts/deckconfig/deckconfig-base.scss
Normal file
15
ts/deckconfig/deckconfig-base.scss
Normal file
@ -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;
|
||||||
|
}
|
19
ts/deckconfig/deckconfig.html
Normal file
19
ts/deckconfig/deckconfig.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" id="viewport" content="width=device-width" />
|
||||||
|
<link href="deckconfig-base.css" rel="stylesheet" />
|
||||||
|
<link href="deckconfig.css" rel="stylesheet" />
|
||||||
|
<script src="../js/vendor/protobuf.min.js"></script>
|
||||||
|
<script src="../js/vendor/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="deckconfig.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
anki.deckConfig(document.getElementById("main"), 1);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
4
ts/deckconfig/events.ts
Normal file
4
ts/deckconfig/events.ts
Normal file
@ -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 } };
|
7
ts/deckconfig/icons.ts
Normal file
7
ts/deckconfig/icons.ts
Normal file
@ -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 };
|
23
ts/deckconfig/index.ts
Normal file
23
ts/deckconfig/index.ts
Normal file
@ -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<void> {
|
||||||
|
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 },
|
||||||
|
});
|
||||||
|
}
|
52
ts/deckconfig/lib.ts
Normal file
52
ts/deckconfig/lib.ts
Normal file
@ -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<pb.BackendProto.DeckConfigForUpdate> {
|
||||||
|
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<DeckConfigId, string>;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
23
ts/deckconfig/steps.test.ts
Normal file
23
ts/deckconfig/steps.test.ts
Normal file
@ -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]);
|
||||||
|
});
|
70
ts/deckconfig/steps.ts
Normal file
70
ts/deckconfig/steps.ts
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
@ -6,14 +6,17 @@ load("//ts:vendor.bzl", "copy_bootstrap_icons")
|
|||||||
load("//ts:compile_sass.bzl", "compile_sass")
|
load("//ts:compile_sass.bzl", "compile_sass")
|
||||||
|
|
||||||
compile_sass(
|
compile_sass(
|
||||||
|
srcs = [
|
||||||
|
"editable.scss",
|
||||||
|
"editor.scss",
|
||||||
|
],
|
||||||
group = "base_css",
|
group = "base_css",
|
||||||
srcs = ["editor.scss", "editable.scss"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"//ts/sass:base_lib",
|
"//ts/sass:base_lib",
|
||||||
"//ts/sass:buttons_lib",
|
"//ts/sass:buttons_lib",
|
||||||
"//ts/sass:scrollbar_lib",
|
"//ts/sass:scrollbar_lib",
|
||||||
],
|
],
|
||||||
visibility = ["//visibility:public"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ts_library(
|
ts_library(
|
||||||
@ -41,9 +44,9 @@ esbuild(
|
|||||||
entry_point = "index_wrapper.ts",
|
entry_point = "index_wrapper.ts",
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"base_css",
|
||||||
":bootstrap-icons",
|
":bootstrap-icons",
|
||||||
":editor_ts",
|
":editor_ts",
|
||||||
"base_css",
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user