mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Deck-specific Limits (#1955)
* Add deck-specific limits to DeckNormal * Add deck-specific limits to schema11 * Add DeckLimitsDialog * deck_limits_qt6.py needs to be a symlink * Clear duplicate deck setting keys on downgrade * Export deck limits when exporting with scheduling * Revert "deck_limits_qt6.py needs to be a symlink" This reverts commit4ee7be1e10
. * Revert "Add DeckLimitsDialog" This reverts commiteb0e2a62d3
. * Add day limits to DeckNormal * Add deck and day limits mock to deck options * Revert "Add deck and day limits mock to deck options" This reverts commit0775814989
. * Add Tabs component for daily limits * Add borders to tabs component * Revert "Add borders to tabs component" This reverts commitaaaf553893
. * Implement tabbed limits properly * Add comment to translations * Update rslib/src/decks/limits.rs Co-authored-by: Damien Elmes <dae@users.noreply.github.com> * Fix camel case in clear_other_duplicates() * day_limit → current_limit * Also import day limits * Remember last used day limits * Add day limits to schema 11 * Tweak comment (dae) * Exclude day limit in export (dae) * Tweak tab wording (dae) * Update preset limits on preset change * Explain tabs in tooltip (dae) * Omit deck and today limits if v2 is enabled * Preserve deck limit when switching to today limit
This commit is contained in:
parent
e0368a3858
commit
cc929687ae
18 changed files with 432 additions and 31 deletions
|
@ -35,6 +35,17 @@ deck-config-limit-new-bound-by-reviews =
|
||||||
deck-config-limit-interday-bound-by-reviews =
|
deck-config-limit-interday-bound-by-reviews =
|
||||||
The review limit also affects interday learning cards. When applying the limit,
|
The review limit also affects interday learning cards. When applying the limit,
|
||||||
interday learning cards are fetched first, then reviews, and finally new cards.
|
interday learning cards are fetched first, then reviews, and finally new cards.
|
||||||
|
deck-config-tab-description =
|
||||||
|
- `Preset`: The limit is shared with all decks using this preset.
|
||||||
|
- `This deck`: The limit is specific to this deck.
|
||||||
|
- `Today only`: Make a temporary change to this deck's limit.
|
||||||
|
|
||||||
|
## Daily limit tabs: please try to keep these as short as the English version,
|
||||||
|
## as longer text will not fit on small screens.
|
||||||
|
|
||||||
|
deck-config-shared-preset = Preset
|
||||||
|
deck-config-deck-only = This deck
|
||||||
|
deck-config-today-only = Today only
|
||||||
|
|
||||||
## New Cards section
|
## New Cards section
|
||||||
|
|
||||||
|
|
|
@ -144,9 +144,20 @@ message DeckConfigsForUpdate {
|
||||||
uint32 use_count = 2;
|
uint32 use_count = 2;
|
||||||
}
|
}
|
||||||
message CurrentDeck {
|
message CurrentDeck {
|
||||||
|
message Limits {
|
||||||
|
optional uint32 review = 1;
|
||||||
|
optional uint32 new = 2;
|
||||||
|
optional uint32 review_today = 3;
|
||||||
|
optional uint32 new_today = 4;
|
||||||
|
// Whether review_today applies to today or a past day.
|
||||||
|
bool review_today_active = 5;
|
||||||
|
// Whether new_today applies to today or a past day.
|
||||||
|
bool new_today_active = 6;
|
||||||
|
}
|
||||||
string name = 1;
|
string name = 1;
|
||||||
int64 config_id = 2;
|
int64 config_id = 2;
|
||||||
repeated int64 parent_config_ids = 3;
|
repeated int64 parent_config_ids = 3;
|
||||||
|
Limits limits = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
repeated ConfigWithExtra all_config = 1;
|
repeated ConfigWithExtra all_config = 1;
|
||||||
|
@ -167,4 +178,5 @@ message UpdateDeckConfigsRequest {
|
||||||
repeated int64 removed_config_ids = 3;
|
repeated int64 removed_config_ids = 3;
|
||||||
bool apply_to_children = 4;
|
bool apply_to_children = 4;
|
||||||
string card_state_customizer = 5;
|
string card_state_customizer = 5;
|
||||||
|
DeckConfigsForUpdate.CurrentDeck.Limits limits = 6;
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,13 +64,21 @@ message Deck {
|
||||||
bytes other = 255;
|
bytes other = 255;
|
||||||
}
|
}
|
||||||
message Normal {
|
message Normal {
|
||||||
|
message DayLimit {
|
||||||
|
uint32 limit = 1;
|
||||||
|
uint32 today = 2;
|
||||||
|
}
|
||||||
int64 config_id = 1;
|
int64 config_id = 1;
|
||||||
uint32 extend_new = 2;
|
uint32 extend_new = 2;
|
||||||
uint32 extend_review = 3;
|
uint32 extend_review = 3;
|
||||||
string description = 4;
|
string description = 4;
|
||||||
bool markdown_description = 5;
|
bool markdown_description = 5;
|
||||||
|
optional uint32 review_limit = 6;
|
||||||
|
optional uint32 new_limit = 7;
|
||||||
|
DayLimit review_limit_today = 8;
|
||||||
|
DayLimit new_limit_today = 9;
|
||||||
|
|
||||||
reserved 6 to 11;
|
reserved 12 to 15;
|
||||||
}
|
}
|
||||||
message Filtered {
|
message Filtered {
|
||||||
message SearchTerm {
|
message SearchTerm {
|
||||||
|
|
|
@ -270,6 +270,8 @@ class AnkiExporter(Exporter):
|
||||||
# scheduling not included, so reset deck settings to default
|
# scheduling not included, so reset deck settings to default
|
||||||
d = dict(d)
|
d = dict(d)
|
||||||
d["conf"] = 1
|
d["conf"] = 1
|
||||||
|
d["reviewLimit"] = d["newLimit"] = None
|
||||||
|
d["reviewLimitToday"] = d["newLimitToday"] = None
|
||||||
self.dst.decks.update(d)
|
self.dst.decks.update(d)
|
||||||
# copy used deck confs
|
# copy used deck confs
|
||||||
for dc in self.src.decks.all_config():
|
for dc in self.src.decks.all_config():
|
||||||
|
|
|
@ -105,6 +105,10 @@ pub fn write_backend_proto_rs() {
|
||||||
"Deck.Filtered.SearchTerm.Order",
|
"Deck.Filtered.SearchTerm.Order",
|
||||||
"#[derive(strum::EnumIter)]",
|
"#[derive(strum::EnumIter)]",
|
||||||
)
|
)
|
||||||
|
.type_attribute(
|
||||||
|
"Deck.Normal.DayLimit",
|
||||||
|
"#[derive(Copy, serde_derive::Deserialize, serde_derive::Serialize)]",
|
||||||
|
)
|
||||||
.type_attribute("HelpPageLinkRequest.HelpPage", "#[derive(strum::EnumIter)]")
|
.type_attribute("HelpPageLinkRequest.HelpPage", "#[derive(strum::EnumIter)]")
|
||||||
.type_attribute("CsvMetadata.Delimiter", "#[derive(strum::EnumIter)]")
|
.type_attribute("CsvMetadata.Delimiter", "#[derive(strum::EnumIter)]")
|
||||||
.type_attribute(
|
.type_attribute(
|
||||||
|
|
|
@ -89,6 +89,7 @@ impl From<pb::UpdateDeckConfigsRequest> for UpdateDeckConfigsRequest {
|
||||||
removed_config_ids: c.removed_config_ids.into_iter().map(Into::into).collect(),
|
removed_config_ids: c.removed_config_ids.into_iter().map(Into::into).collect(),
|
||||||
apply_to_children: c.apply_to_children,
|
apply_to_children: c.apply_to_children,
|
||||||
card_state_customizer: c.card_state_customizer,
|
card_state_customizer: c.card_state_customizer,
|
||||||
|
limits: c.limits.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,12 @@ use std::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::StringKey,
|
config::StringKey,
|
||||||
|
decks::NormalDeck,
|
||||||
pb,
|
pb,
|
||||||
pb::deck_configs_for_update::{ConfigWithExtra, CurrentDeck},
|
pb::{
|
||||||
|
deck::normal::DayLimit,
|
||||||
|
deck_configs_for_update::{current_deck::Limits, ConfigWithExtra, CurrentDeck},
|
||||||
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
search::{JoinSearches, SearchNode},
|
search::{JoinSearches, SearchNode},
|
||||||
};
|
};
|
||||||
|
@ -24,6 +28,7 @@ pub struct UpdateDeckConfigsRequest {
|
||||||
pub removed_config_ids: Vec<DeckConfigId>,
|
pub removed_config_ids: Vec<DeckConfigId>,
|
||||||
pub apply_to_children: bool,
|
pub apply_to_children: bool,
|
||||||
pub card_state_customizer: String,
|
pub card_state_customizer: String,
|
||||||
|
pub limits: Limits,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
|
@ -84,15 +89,18 @@ impl Collection {
|
||||||
|
|
||||||
fn get_current_deck_for_update(&mut self, deck: DeckId) -> Result<CurrentDeck> {
|
fn get_current_deck_for_update(&mut self, deck: DeckId) -> Result<CurrentDeck> {
|
||||||
let deck = self.get_deck(deck)?.ok_or(AnkiError::NotFound)?;
|
let deck = self.get_deck(deck)?.ok_or(AnkiError::NotFound)?;
|
||||||
|
let normal = deck.normal()?;
|
||||||
|
let today = self.timing_today()?.days_elapsed;
|
||||||
|
|
||||||
Ok(CurrentDeck {
|
Ok(CurrentDeck {
|
||||||
name: deck.human_name(),
|
name: deck.human_name(),
|
||||||
config_id: deck.normal()?.config_id,
|
config_id: normal.config_id,
|
||||||
parent_config_ids: self
|
parent_config_ids: self
|
||||||
.parent_config_ids(&deck)?
|
.parent_config_ids(&deck)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.collect(),
|
.collect(),
|
||||||
|
limits: Some(normal.to_limits(today)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,6 +155,7 @@ impl Collection {
|
||||||
|
|
||||||
// loop through all normal decks
|
// loop through all normal decks
|
||||||
let usn = self.usn()?;
|
let usn = self.usn()?;
|
||||||
|
let today = self.timing_today()?.days_elapsed;
|
||||||
let selected_config = input.configs.last().unwrap();
|
let selected_config = input.configs.last().unwrap();
|
||||||
for deck in self.storage.get_all_decks()? {
|
for deck in self.storage.get_all_decks()? {
|
||||||
if let Ok(normal) = deck.normal() {
|
if let Ok(normal) = deck.normal() {
|
||||||
|
@ -165,6 +174,7 @@ impl Collection {
|
||||||
{
|
{
|
||||||
let mut updated = deck.clone();
|
let mut updated = deck.clone();
|
||||||
updated.normal_mut()?.config_id = selected_config.id.0;
|
updated.normal_mut()?.config_id = selected_config.id.0;
|
||||||
|
updated.normal_mut()?.update_limits(&input.limits, today);
|
||||||
self.update_deck_inner(&mut updated, deck, usn)?;
|
self.update_deck_inner(&mut updated, deck, usn)?;
|
||||||
selected_config.id
|
selected_config.id
|
||||||
} else {
|
} else {
|
||||||
|
@ -223,6 +233,42 @@ impl Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl NormalDeck {
|
||||||
|
fn to_limits(&self, today: u32) -> Limits {
|
||||||
|
Limits {
|
||||||
|
review: self.review_limit,
|
||||||
|
new: self.new_limit,
|
||||||
|
review_today: self.review_limit_today.map(|limit| limit.limit),
|
||||||
|
new_today: self.new_limit_today.map(|limit| limit.limit),
|
||||||
|
review_today_active: self
|
||||||
|
.review_limit_today
|
||||||
|
.map(|limit| limit.today == today)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
new_today_active: self
|
||||||
|
.new_limit_today
|
||||||
|
.map(|limit| limit.today == today)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_limits(&mut self, limits: &Limits, today: u32) {
|
||||||
|
self.review_limit = limits.review;
|
||||||
|
self.new_limit = limits.new;
|
||||||
|
update_day_limit(&mut self.review_limit_today, limits.review_today, today);
|
||||||
|
update_day_limit(&mut self.new_limit_today, limits.new_today, today);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_day_limit(day_limit: &mut Option<DayLimit>, new_limit: Option<u32>, today: u32) {
|
||||||
|
if let Some(limit) = new_limit {
|
||||||
|
day_limit.replace(DayLimit { limit, today });
|
||||||
|
} else if let Some(limit) = day_limit {
|
||||||
|
// instead of setting to None, only make sure today is in the past,
|
||||||
|
// thus preserving last used value
|
||||||
|
limit.today = limit.today.min(today - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -279,6 +325,7 @@ mod test {
|
||||||
removed_config_ids: vec![],
|
removed_config_ids: vec![],
|
||||||
apply_to_children: false,
|
apply_to_children: false,
|
||||||
card_state_customizer: "".to_string(),
|
card_state_customizer: "".to_string(),
|
||||||
|
limits: Limits::default(),
|
||||||
};
|
};
|
||||||
assert!(!col.update_deck_configs(input.clone())?.changes.had_change());
|
assert!(!col.update_deck_configs(input.clone())?.changes.had_change());
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,43 @@ use std::{collections::HashMap, iter::Peekable};
|
||||||
|
|
||||||
use id_tree::{InsertBehavior, Node, NodeId, Tree};
|
use id_tree::{InsertBehavior, Node, NodeId, Tree};
|
||||||
|
|
||||||
use super::Deck;
|
use super::{Deck, NormalDeck};
|
||||||
use crate::{
|
use crate::{
|
||||||
deckconfig::{DeckConfig, DeckConfigId},
|
deckconfig::{DeckConfig, DeckConfigId},
|
||||||
|
pb::decks::deck::normal::DayLimit,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
impl NormalDeck {
|
||||||
|
/// The deck's review limit for today, or its regular one, if any is configured.
|
||||||
|
pub fn current_review_limit(&self, today: u32) -> Option<u32> {
|
||||||
|
self.review_limit_today(today).or(self.review_limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The deck's new limit for today, or its regular one, if any is configured.
|
||||||
|
pub fn current_new_limit(&self, today: u32) -> Option<u32> {
|
||||||
|
self.new_limit_today(today).or(self.new_limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The deck's review limit for today.
|
||||||
|
pub fn review_limit_today(&self, today: u32) -> Option<u32> {
|
||||||
|
self.review_limit_today
|
||||||
|
.and_then(|day_limit| day_limit.limit(today))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The deck's new limit for today.
|
||||||
|
pub fn new_limit_today(&self, today: u32) -> Option<u32> {
|
||||||
|
self.new_limit_today
|
||||||
|
.and_then(|day_limit| day_limit.limit(today))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DayLimit {
|
||||||
|
pub fn limit(&self, today: u32) -> Option<u32> {
|
||||||
|
(self.today == today).then(|| self.limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub(crate) struct RemainingLimits {
|
pub(crate) struct RemainingLimits {
|
||||||
pub review: u32,
|
pub review: u32,
|
||||||
|
@ -19,19 +50,37 @@ pub(crate) struct RemainingLimits {
|
||||||
|
|
||||||
impl RemainingLimits {
|
impl RemainingLimits {
|
||||||
pub(crate) fn new(deck: &Deck, config: Option<&DeckConfig>, today: u32, v3: bool) -> Self {
|
pub(crate) fn new(deck: &Deck, config: Option<&DeckConfig>, today: u32, v3: bool) -> Self {
|
||||||
config
|
if let Ok(normal) = deck.normal() {
|
||||||
.map(|config| {
|
if let Some(config) = config {
|
||||||
let (new_today, mut rev_today) = deck.new_rev_counts(today);
|
return Self::new_for_normal_deck(deck, today, v3, normal, config);
|
||||||
if v3 {
|
}
|
||||||
// any reviewed new cards contribute to the review limit
|
}
|
||||||
rev_today += new_today;
|
Self::default()
|
||||||
}
|
}
|
||||||
RemainingLimits {
|
|
||||||
review: ((config.inner.reviews_per_day as i32) - rev_today).max(0) as u32,
|
fn new_for_normal_deck(
|
||||||
new: ((config.inner.new_per_day as i32) - new_today).max(0) as u32,
|
deck: &Deck,
|
||||||
}
|
today: u32,
|
||||||
})
|
v3: bool,
|
||||||
.unwrap_or_default()
|
normal: &NormalDeck,
|
||||||
|
config: &DeckConfig,
|
||||||
|
) -> RemainingLimits {
|
||||||
|
let review_limit = normal
|
||||||
|
.current_review_limit(today)
|
||||||
|
.unwrap_or(config.inner.reviews_per_day);
|
||||||
|
let new_limit = normal
|
||||||
|
.current_new_limit(today)
|
||||||
|
.unwrap_or(config.inner.new_per_day);
|
||||||
|
let (new_today, mut rev_today) = deck.new_rev_counts(today);
|
||||||
|
if v3 {
|
||||||
|
// any reviewed new cards contribute to the review limit
|
||||||
|
rev_today += new_today;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
review: (review_limit as i32 - rev_today).max(0) as u32,
|
||||||
|
new: (new_limit as i32 - new_today).max(0) as u32,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cap_to(&mut self, limits: RemainingLimits) {
|
pub(crate) fn cap_to(&mut self, limits: RemainingLimits) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ use serde_tuple::Serialize_tuple;
|
||||||
|
|
||||||
use super::{DeckCommon, FilteredDeck, FilteredSearchTerm, NormalDeck};
|
use super::{DeckCommon, FilteredDeck, FilteredSearchTerm, NormalDeck};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
pb::decks::deck::normal::DayLimit,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
serde::{default_on_invalid, deserialize_bool_from_anything, deserialize_number_from_string},
|
serde::{default_on_invalid, deserialize_bool_from_anything, deserialize_number_from_string},
|
||||||
};
|
};
|
||||||
|
@ -116,6 +117,14 @@ pub struct NormalDeckSchema11 {
|
||||||
extend_new: i32,
|
extend_new: i32,
|
||||||
#[serde(default, deserialize_with = "default_on_invalid")]
|
#[serde(default, deserialize_with = "default_on_invalid")]
|
||||||
extend_rev: i32,
|
extend_rev: i32,
|
||||||
|
#[serde(default, deserialize_with = "default_on_invalid")]
|
||||||
|
review_limit: Option<u32>,
|
||||||
|
#[serde(default, deserialize_with = "default_on_invalid")]
|
||||||
|
new_limit: Option<u32>,
|
||||||
|
#[serde(default, deserialize_with = "default_on_invalid")]
|
||||||
|
review_limit_today: Option<DayLimit>,
|
||||||
|
#[serde(default, deserialize_with = "default_on_invalid")]
|
||||||
|
new_limit_today: Option<DayLimit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||||
|
@ -226,6 +235,10 @@ impl Default for NormalDeckSchema11 {
|
||||||
conf: 1,
|
conf: 1,
|
||||||
extend_new: 0,
|
extend_new: 0,
|
||||||
extend_rev: 0,
|
extend_rev: 0,
|
||||||
|
review_limit: None,
|
||||||
|
new_limit: None,
|
||||||
|
review_limit_today: None,
|
||||||
|
new_limit_today: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,6 +311,10 @@ impl From<NormalDeckSchema11> for NormalDeck {
|
||||||
extend_review: deck.extend_rev.max(0) as u32,
|
extend_review: deck.extend_rev.max(0) as u32,
|
||||||
markdown_description: deck.common.markdown_description,
|
markdown_description: deck.common.markdown_description,
|
||||||
description: deck.common.desc,
|
description: deck.common.desc,
|
||||||
|
review_limit: deck.review_limit,
|
||||||
|
new_limit: deck.new_limit,
|
||||||
|
review_limit_today: deck.review_limit_today,
|
||||||
|
new_limit_today: deck.new_limit_today,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -332,6 +349,10 @@ impl From<Deck> for DeckSchema11 {
|
||||||
conf: norm.config_id,
|
conf: norm.config_id,
|
||||||
extend_new: norm.extend_new as i32,
|
extend_new: norm.extend_new as i32,
|
||||||
extend_rev: norm.extend_review as i32,
|
extend_rev: norm.extend_review as i32,
|
||||||
|
review_limit: norm.review_limit,
|
||||||
|
new_limit: norm.new_limit,
|
||||||
|
review_limit_today: norm.review_limit_today,
|
||||||
|
new_limit_today: norm.new_limit_today,
|
||||||
common: deck.into(),
|
common: deck.into(),
|
||||||
}),
|
}),
|
||||||
DeckKind::Filtered(ref filt) => DeckSchema11::Filtered(FilteredDeckSchema11 {
|
DeckKind::Filtered(ref filt) => DeckSchema11::Filtered(FilteredDeckSchema11 {
|
||||||
|
@ -352,11 +373,12 @@ impl From<Deck> for DeckSchema11 {
|
||||||
|
|
||||||
impl From<Deck> for DeckCommonSchema11 {
|
impl From<Deck> for DeckCommonSchema11 {
|
||||||
fn from(deck: Deck) -> Self {
|
fn from(deck: Deck) -> Self {
|
||||||
let other: HashMap<String, Value> = if deck.common.other.is_empty() {
|
let mut other: HashMap<String, Value> = if deck.common.other.is_empty() {
|
||||||
Default::default()
|
Default::default()
|
||||||
} else {
|
} else {
|
||||||
serde_json::from_slice(&deck.common.other).unwrap_or_default()
|
serde_json::from_slice(&deck.common.other).unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
clear_other_duplicates(&mut other);
|
||||||
DeckCommonSchema11 {
|
DeckCommonSchema11 {
|
||||||
id: deck.id,
|
id: deck.id,
|
||||||
mtime: deck.mtime_secs,
|
mtime: deck.mtime_secs,
|
||||||
|
@ -383,6 +405,18 @@ impl From<Deck> for DeckCommonSchema11 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See [crate::deckconfig::schema11::clear_other_duplicates()].
|
||||||
|
fn clear_other_duplicates(other: &mut HashMap<String, Value>) {
|
||||||
|
for key in [
|
||||||
|
"reviewLimit",
|
||||||
|
"newLimit",
|
||||||
|
"reviewLimitToday",
|
||||||
|
"newLimitToday",
|
||||||
|
] {
|
||||||
|
other.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&Deck> for DeckTodaySchema11 {
|
impl From<&Deck> for DeckTodaySchema11 {
|
||||||
fn from(deck: &Deck) -> Self {
|
fn from(deck: &Deck) -> Self {
|
||||||
let day = deck.common.last_day_studied as i32;
|
let day = deck.common.last_day_studied as i32;
|
||||||
|
|
|
@ -77,7 +77,7 @@ impl ExchangeData {
|
||||||
|
|
||||||
fn remove_scheduling_information(&mut self, col: &Collection) {
|
fn remove_scheduling_information(&mut self, col: &Collection) {
|
||||||
self.remove_system_tags();
|
self.remove_system_tags();
|
||||||
self.reset_deck_config_ids();
|
self.reset_deck_config_ids_and_limits();
|
||||||
self.reset_cards(col);
|
self.reset_cards(col);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,10 +91,14 @@ impl ExchangeData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_deck_config_ids(&mut self) {
|
fn reset_deck_config_ids_and_limits(&mut self) {
|
||||||
for deck in self.decks.iter_mut() {
|
for deck in self.decks.iter_mut() {
|
||||||
if let Ok(normal_mut) = deck.normal_mut() {
|
if let Ok(normal_mut) = deck.normal_mut() {
|
||||||
normal_mut.config_id = 1;
|
normal_mut.config_id = 1;
|
||||||
|
normal_mut.review_limit = None;
|
||||||
|
normal_mut.review_limit_today = None;
|
||||||
|
normal_mut.new_limit = None;
|
||||||
|
normal_mut.new_limit_today = None;
|
||||||
} else {
|
} else {
|
||||||
// filtered decks are reset at import time for legacy reasons
|
// filtered decks are reset at import time for legacy reasons
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,6 +163,10 @@ impl NormalDeck {
|
||||||
if other.config_id != 1 {
|
if other.config_id != 1 {
|
||||||
self.config_id = other.config_id;
|
self.config_id = other.config_id;
|
||||||
}
|
}
|
||||||
|
self.review_limit = other.review_limit.or(self.review_limit);
|
||||||
|
self.new_limit = other.new_limit.or(self.new_limit);
|
||||||
|
self.review_limit_today = other.review_limit_today.or(self.review_limit_today);
|
||||||
|
self.new_limit_today = other.new_limit_today.or(self.new_limit_today);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -159,8 +159,8 @@ impl From<Notetype> for NotetypeSchema11 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See [crate::deckconfig::schema11::clear_other_duplicates()].
|
||||||
fn clear_other_field_duplicates(other: &mut HashMap<String, Value>) {
|
fn clear_other_field_duplicates(other: &mut HashMap<String, Value>) {
|
||||||
// see `clear_other_duplicates()` in `deckconfig/schema11.rs`
|
|
||||||
for key in &["description"] {
|
for key in &["description"] {
|
||||||
other.remove(*key);
|
other.remove(*key);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type Modal from "bootstrap/js/dist/modal";
|
import type Modal from "bootstrap/js/dist/modal";
|
||||||
import { getContext } from "svelte";
|
import { createEventDispatcher, getContext } from "svelte";
|
||||||
|
|
||||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||||
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
||||||
|
@ -20,6 +20,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
export let state: DeckOptionsState;
|
||||||
const configList = state.configList;
|
const configList = state.configList;
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const dispatchPresetChange = () => dispatch("presetchange");
|
||||||
|
|
||||||
function configLabel(entry: ConfigListEntry): string {
|
function configLabel(entry: ConfigListEntry): string {
|
||||||
const count = tr.deckConfigUsedByDecks({ decks: entry.useCount });
|
const count = tr.deckConfigUsedByDecks({ decks: entry.useCount });
|
||||||
|
@ -28,12 +30,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
function blur(event: Event): void {
|
function blur(event: Event): void {
|
||||||
state.setCurrentIndex(parseInt((event.target! as HTMLSelectElement).value));
|
state.setCurrentIndex(parseInt((event.target! as HTMLSelectElement).value));
|
||||||
|
dispatchPresetChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAddConfig(text: string): void {
|
function onAddConfig(text: string): void {
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
if (trimmed.length) {
|
if (trimmed.length) {
|
||||||
state.addConfig(trimmed);
|
state.addConfig(trimmed);
|
||||||
|
dispatchPresetChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +45,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
if (trimmed.length) {
|
if (trimmed.length) {
|
||||||
state.cloneConfig(trimmed);
|
state.cloneConfig(trimmed);
|
||||||
|
dispatchPresetChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +112,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
on:add={promptToAdd}
|
on:add={promptToAdd}
|
||||||
on:clone={promptToClone}
|
on:clone={promptToClone}
|
||||||
on:rename={promptToRename}
|
on:rename={promptToRename}
|
||||||
|
on:remove={dispatchPresetChange}
|
||||||
/>
|
/>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
</StickyContainer>
|
</StickyContainer>
|
||||||
|
|
|
@ -7,14 +7,34 @@
|
||||||
import Item from "../components/Item.svelte";
|
import Item from "../components/Item.svelte";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
|
import { ValueTab } from "./lib";
|
||||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||||
|
import TabbedValue from "./TabbedValue.svelte";
|
||||||
import TitledContainer from "./TitledContainer.svelte";
|
import TitledContainer from "./TitledContainer.svelte";
|
||||||
import Warning from "./Warning.svelte";
|
import Warning from "./Warning.svelte";
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
export let state: DeckOptionsState;
|
||||||
export let api: Record<string, never>;
|
export let api: Record<string, never>;
|
||||||
|
|
||||||
|
export function onPresetChange() {
|
||||||
|
newTabs[0] = new ValueTab(
|
||||||
|
tr.deckConfigSharedPreset(),
|
||||||
|
$config.newPerDay,
|
||||||
|
(value) => ($config.newPerDay = value!),
|
||||||
|
$config.newPerDay,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
reviewTabs[0] = new ValueTab(
|
||||||
|
tr.deckConfigSharedPreset(),
|
||||||
|
$config.reviewsPerDay,
|
||||||
|
(value) => ($config.reviewsPerDay = value!),
|
||||||
|
$config.reviewsPerDay,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const config = state.currentConfig;
|
const config = state.currentConfig;
|
||||||
|
const limits = state.deckLimits;
|
||||||
const defaults = state.defaults;
|
const defaults = state.defaults;
|
||||||
const parentLimits = state.parentLimits;
|
const parentLimits = state.parentLimits;
|
||||||
|
|
||||||
|
@ -24,28 +44,92 @@
|
||||||
"\n\n" +
|
"\n\n" +
|
||||||
tr.deckConfigLimitInterdayBoundByReviews() +
|
tr.deckConfigLimitInterdayBoundByReviews() +
|
||||||
"\n\n" +
|
"\n\n" +
|
||||||
tr.deckConfigLimitDeckV3()
|
tr.deckConfigLimitDeckV3() +
|
||||||
|
"\n\n" +
|
||||||
|
tr.deckConfigTabDescription()
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
$: newCardsGreaterThanParent =
|
$: newCardsGreaterThanParent =
|
||||||
!state.v3Scheduler && $config.newPerDay > $parentLimits.newCards
|
!state.v3Scheduler && newValue > $parentLimits.newCards
|
||||||
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
|
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
$: reviewsTooLow =
|
$: reviewsTooLow =
|
||||||
Math.min(9999, $config.newPerDay * 10) > $config.reviewsPerDay
|
Math.min(9999, newValue * 10) > reviewsValue
|
||||||
? tr.deckConfigReviewsTooLow({
|
? tr.deckConfigReviewsTooLow({
|
||||||
cards: $config.newPerDay,
|
cards: newValue,
|
||||||
expected: Math.min(9999, $config.newPerDay * 10),
|
expected: Math.min(9999, newValue * 10),
|
||||||
})
|
})
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const newTabs: ValueTab[] = [
|
||||||
|
new ValueTab(
|
||||||
|
tr.deckConfigSharedPreset(),
|
||||||
|
$config.newPerDay,
|
||||||
|
(value) => ($config.newPerDay = value!),
|
||||||
|
$config.newPerDay,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
].concat(
|
||||||
|
state.v3Scheduler
|
||||||
|
? [
|
||||||
|
new ValueTab(
|
||||||
|
tr.deckConfigDeckOnly(),
|
||||||
|
$limits.new ?? null,
|
||||||
|
(value) => ($limits.new = value),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
new ValueTab(
|
||||||
|
tr.deckConfigTodayOnly(),
|
||||||
|
$limits.newTodayActive ? $limits.newToday ?? null : null,
|
||||||
|
(value) => ($limits.newToday = value),
|
||||||
|
null,
|
||||||
|
$limits.newToday ?? null,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reviewTabs: ValueTab[] = [
|
||||||
|
new ValueTab(
|
||||||
|
tr.deckConfigSharedPreset(),
|
||||||
|
$config.reviewsPerDay,
|
||||||
|
(value) => ($config.reviewsPerDay = value!),
|
||||||
|
$config.reviewsPerDay,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
].concat(
|
||||||
|
state.v3Scheduler
|
||||||
|
? [
|
||||||
|
new ValueTab(
|
||||||
|
tr.deckConfigDeckOnly(),
|
||||||
|
$limits.review ?? null,
|
||||||
|
(value) => ($limits.review = value),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
new ValueTab(
|
||||||
|
tr.deckConfigTodayOnly(),
|
||||||
|
$limits.reviewTodayActive ? $limits.reviewToday ?? null : null,
|
||||||
|
(value) => ($limits.reviewToday = value),
|
||||||
|
null,
|
||||||
|
$limits.reviewToday ?? null,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
let reviewsValue: number;
|
||||||
|
let newValue: number;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.deckConfigDailyLimits()}>
|
<TitledContainer title={tr.deckConfigDailyLimits()}>
|
||||||
<DynamicallySlottable slotHost={Item} {api}>
|
<DynamicallySlottable slotHost={Item} {api}>
|
||||||
|
<TabbedValue tabs={newTabs} bind:value={newValue} />
|
||||||
<Item>
|
<Item>
|
||||||
<SpinBoxRow
|
<SpinBoxRow
|
||||||
bind:value={$config.newPerDay}
|
bind:value={newValue}
|
||||||
defaultValue={defaults.newPerDay}
|
defaultValue={defaults.newPerDay}
|
||||||
markdownTooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
|
markdownTooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
|
||||||
>
|
>
|
||||||
|
@ -57,9 +141,10 @@
|
||||||
<Warning warning={newCardsGreaterThanParent} />
|
<Warning warning={newCardsGreaterThanParent} />
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
|
<TabbedValue tabs={reviewTabs} bind:value={reviewsValue} />
|
||||||
<Item>
|
<Item>
|
||||||
<SpinBoxRow
|
<SpinBoxRow
|
||||||
bind:value={$config.reviewsPerDay}
|
bind:value={reviewsValue}
|
||||||
defaultValue={defaults.reviewsPerDay}
|
defaultValue={defaults.reviewsPerDay}
|
||||||
markdownTooltip={tr.deckConfigReviewLimitTooltip() + v3Extra}
|
markdownTooltip={tr.deckConfigReviewLimitTooltip() + v3Extra}
|
||||||
>
|
>
|
||||||
|
|
|
@ -54,9 +54,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export const timerOptions = {};
|
export const timerOptions = {};
|
||||||
export const audioOptions = {};
|
export const audioOptions = {};
|
||||||
export const advancedOptions = {};
|
export const advancedOptions = {};
|
||||||
|
|
||||||
|
let onPresetChange: () => void;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ConfigSelector {state} />
|
<ConfigSelector {state} on:presetchange={onPresetChange} />
|
||||||
|
|
||||||
<div class="deck-options-page">
|
<div class="deck-options-page">
|
||||||
<Container
|
<Container
|
||||||
|
@ -68,7 +70,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<DynamicallySlottable slotHost={Item} api={options}>
|
<DynamicallySlottable slotHost={Item} api={options}>
|
||||||
<Item>
|
<Item>
|
||||||
<Row class="row-columns">
|
<Row class="row-columns">
|
||||||
<DailyLimits {state} api={dailyLimits} />
|
<DailyLimits {state} api={dailyLimits} bind:onPresetChange />
|
||||||
</Row>
|
</Row>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
if (confirm(withCollapsedWhitespace(msg))) {
|
if (confirm(withCollapsedWhitespace(msg))) {
|
||||||
try {
|
try {
|
||||||
state.removeCurrentConfig();
|
state.removeCurrentConfig();
|
||||||
|
dispatch("remove");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err);
|
alert(err);
|
||||||
}
|
}
|
||||||
|
|
83
ts/deck-options/TabbedValue.svelte
Normal file
83
ts/deck-options/TabbedValue.svelte
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
/* This component accepts an array of tabs and a value. Whenever a tab is
|
||||||
|
activated, its last used value is applied to its provided setter and the
|
||||||
|
component's value. Whenever it's deactivated, its setter is called with its
|
||||||
|
disabledValue. */
|
||||||
|
import type { ValueTab } from "./lib";
|
||||||
|
|
||||||
|
export let tabs: ValueTab[];
|
||||||
|
export let value: number;
|
||||||
|
|
||||||
|
let activeTab = lastSetTab();
|
||||||
|
$: onTabChanged(activeTab);
|
||||||
|
$: value = tabs[activeTab].value ?? 0;
|
||||||
|
$: tabs[activeTab].setValue(value);
|
||||||
|
|
||||||
|
function lastSetTab(): number {
|
||||||
|
const revIdx = tabs
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.findIndex((tab) => tab.value !== null);
|
||||||
|
return revIdx === -1 ? 0 : tabs.length - revIdx - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTabChanged(newTab: number) {
|
||||||
|
for (const [idx, tab] of tabs.entries()) {
|
||||||
|
if (newTab === idx) {
|
||||||
|
tab.enable(value);
|
||||||
|
} else if (newTab > idx) {
|
||||||
|
/* antecedent tabs are obscured, so we can preserve their original values */
|
||||||
|
tab.reset();
|
||||||
|
} else {
|
||||||
|
/* but subsequent tabs would obscure, so they must be nulled */
|
||||||
|
tab.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (tabValue: number) => () => (activeTab = tabValue);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{#each tabs as tab, idx}
|
||||||
|
<li class={activeTab === idx ? "active" : ""}>
|
||||||
|
<span on:click={handleClick(idx)}>{tab.title}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-left: 0;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
list-style: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-top-right-radius: 0.25rem;
|
||||||
|
display: block;
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 8px -1px 0;
|
||||||
|
color: var(--disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
li.active > span {
|
||||||
|
border-color: var(--border) var(--border) var(--window-bg);
|
||||||
|
color: var(--text-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
span:hover {
|
||||||
|
color: var(--text-fg);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -35,6 +35,7 @@ export class DeckOptionsState {
|
||||||
readonly parentLimits: Readable<ParentLimits>;
|
readonly parentLimits: Readable<ParentLimits>;
|
||||||
readonly cardStateCustomizer: Writable<string>;
|
readonly cardStateCustomizer: Writable<string>;
|
||||||
readonly currentDeck: DeckConfig.DeckConfigsForUpdate.CurrentDeck;
|
readonly currentDeck: DeckConfig.DeckConfigsForUpdate.CurrentDeck;
|
||||||
|
readonly deckLimits: Writable<DeckConfig.DeckConfigsForUpdate.CurrentDeck.Limits>;
|
||||||
readonly defaults: DeckConfig.DeckConfig.Config;
|
readonly defaults: DeckConfig.DeckConfig.Config;
|
||||||
readonly addonComponents: Writable<DynamicSvelteComponent[]>;
|
readonly addonComponents: Writable<DynamicSvelteComponent[]>;
|
||||||
readonly v3Scheduler: boolean;
|
readonly v3Scheduler: boolean;
|
||||||
|
@ -68,6 +69,7 @@ export class DeckOptionsState {
|
||||||
this.v3Scheduler = data.v3Scheduler;
|
this.v3Scheduler = data.v3Scheduler;
|
||||||
this.haveAddons = data.haveAddons;
|
this.haveAddons = data.haveAddons;
|
||||||
this.cardStateCustomizer = writable(data.cardStateCustomizer);
|
this.cardStateCustomizer = writable(data.cardStateCustomizer);
|
||||||
|
this.deckLimits = writable(data.currentDeck?.limits ?? createLimits());
|
||||||
|
|
||||||
// decrement the use count of the starting item, as we'll apply +1 to currently
|
// decrement the use count of the starting item, as we'll apply +1 to currently
|
||||||
// selected one at display time
|
// selected one at display time
|
||||||
|
@ -190,6 +192,7 @@ export class DeckOptionsState {
|
||||||
configs,
|
configs,
|
||||||
applyToChildren,
|
applyToChildren,
|
||||||
cardStateCustomizer: get(this.cardStateCustomizer),
|
cardStateCustomizer: get(this.cardStateCustomizer),
|
||||||
|
limits: get(this.deckLimits),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,3 +312,48 @@ function bytesToObject(bytes: Uint8Array): Record<string, unknown> {
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createLimits(): DeckConfig.DeckConfigsForUpdate.CurrentDeck.Limits {
|
||||||
|
return DeckConfig.DeckConfigsForUpdate.CurrentDeck.Limits.create({});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValueTab {
|
||||||
|
readonly title: string;
|
||||||
|
value: number | null;
|
||||||
|
private setter: (value: number | null) => void;
|
||||||
|
private disabledValue: number | null;
|
||||||
|
private startValue: number | null;
|
||||||
|
private initialValue: number | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
title: string,
|
||||||
|
value: number | null,
|
||||||
|
setter: (value: number | null) => void,
|
||||||
|
disabledValue: number | null,
|
||||||
|
startValue: number | null,
|
||||||
|
) {
|
||||||
|
this.title = title;
|
||||||
|
this.value = this.initialValue = value;
|
||||||
|
this.setter = setter;
|
||||||
|
this.disabledValue = disabledValue;
|
||||||
|
this.startValue = startValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.setter(this.initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
disable(): void {
|
||||||
|
this.setter(this.disabledValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
enable(fallbackValue: number): void {
|
||||||
|
this.value = this.value ?? this.startValue ?? fallbackValue;
|
||||||
|
this.setter(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value: number): void {
|
||||||
|
this.value = value;
|
||||||
|
this.setter(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue