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 =
|
||||
The review limit also affects interday learning cards. When applying the limit,
|
||||
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
|
||||
|
||||
|
|
|
@ -144,9 +144,20 @@ message DeckConfigsForUpdate {
|
|||
uint32 use_count = 2;
|
||||
}
|
||||
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;
|
||||
int64 config_id = 2;
|
||||
repeated int64 parent_config_ids = 3;
|
||||
Limits limits = 4;
|
||||
}
|
||||
|
||||
repeated ConfigWithExtra all_config = 1;
|
||||
|
@ -167,4 +178,5 @@ message UpdateDeckConfigsRequest {
|
|||
repeated int64 removed_config_ids = 3;
|
||||
bool apply_to_children = 4;
|
||||
string card_state_customizer = 5;
|
||||
DeckConfigsForUpdate.CurrentDeck.Limits limits = 6;
|
||||
}
|
||||
|
|
|
@ -64,13 +64,21 @@ message Deck {
|
|||
bytes other = 255;
|
||||
}
|
||||
message Normal {
|
||||
message DayLimit {
|
||||
uint32 limit = 1;
|
||||
uint32 today = 2;
|
||||
}
|
||||
int64 config_id = 1;
|
||||
uint32 extend_new = 2;
|
||||
uint32 extend_review = 3;
|
||||
string description = 4;
|
||||
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 SearchTerm {
|
||||
|
|
|
@ -270,6 +270,8 @@ class AnkiExporter(Exporter):
|
|||
# scheduling not included, so reset deck settings to default
|
||||
d = dict(d)
|
||||
d["conf"] = 1
|
||||
d["reviewLimit"] = d["newLimit"] = None
|
||||
d["reviewLimitToday"] = d["newLimitToday"] = None
|
||||
self.dst.decks.update(d)
|
||||
# copy used deck confs
|
||||
for dc in self.src.decks.all_config():
|
||||
|
|
|
@ -105,6 +105,10 @@ pub fn write_backend_proto_rs() {
|
|||
"Deck.Filtered.SearchTerm.Order",
|
||||
"#[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("CsvMetadata.Delimiter", "#[derive(strum::EnumIter)]")
|
||||
.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(),
|
||||
apply_to_children: c.apply_to_children,
|
||||
card_state_customizer: c.card_state_customizer,
|
||||
limits: c.limits.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,12 @@ use std::{
|
|||
|
||||
use crate::{
|
||||
config::StringKey,
|
||||
decks::NormalDeck,
|
||||
pb,
|
||||
pb::deck_configs_for_update::{ConfigWithExtra, CurrentDeck},
|
||||
pb::{
|
||||
deck::normal::DayLimit,
|
||||
deck_configs_for_update::{current_deck::Limits, ConfigWithExtra, CurrentDeck},
|
||||
},
|
||||
prelude::*,
|
||||
search::{JoinSearches, SearchNode},
|
||||
};
|
||||
|
@ -24,6 +28,7 @@ pub struct UpdateDeckConfigsRequest {
|
|||
pub removed_config_ids: Vec<DeckConfigId>,
|
||||
pub apply_to_children: bool,
|
||||
pub card_state_customizer: String,
|
||||
pub limits: Limits,
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
|
@ -84,15 +89,18 @@ impl Collection {
|
|||
|
||||
fn get_current_deck_for_update(&mut self, deck: DeckId) -> Result<CurrentDeck> {
|
||||
let deck = self.get_deck(deck)?.ok_or(AnkiError::NotFound)?;
|
||||
let normal = deck.normal()?;
|
||||
let today = self.timing_today()?.days_elapsed;
|
||||
|
||||
Ok(CurrentDeck {
|
||||
name: deck.human_name(),
|
||||
config_id: deck.normal()?.config_id,
|
||||
config_id: normal.config_id,
|
||||
parent_config_ids: self
|
||||
.parent_config_ids(&deck)?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
limits: Some(normal.to_limits(today)),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -147,6 +155,7 @@ impl Collection {
|
|||
|
||||
// loop through all normal decks
|
||||
let usn = self.usn()?;
|
||||
let today = self.timing_today()?.days_elapsed;
|
||||
let selected_config = input.configs.last().unwrap();
|
||||
for deck in self.storage.get_all_decks()? {
|
||||
if let Ok(normal) = deck.normal() {
|
||||
|
@ -165,6 +174,7 @@ impl Collection {
|
|||
{
|
||||
let mut updated = deck.clone();
|
||||
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)?;
|
||||
selected_config.id
|
||||
} 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)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -279,6 +325,7 @@ mod test {
|
|||
removed_config_ids: vec![],
|
||||
apply_to_children: false,
|
||||
card_state_customizer: "".to_string(),
|
||||
limits: Limits::default(),
|
||||
};
|
||||
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 super::Deck;
|
||||
use super::{Deck, NormalDeck};
|
||||
use crate::{
|
||||
deckconfig::{DeckConfig, DeckConfigId},
|
||||
pb::decks::deck::normal::DayLimit,
|
||||
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)]
|
||||
pub(crate) struct RemainingLimits {
|
||||
pub review: u32,
|
||||
|
@ -19,19 +50,37 @@ pub(crate) struct RemainingLimits {
|
|||
|
||||
impl RemainingLimits {
|
||||
pub(crate) fn new(deck: &Deck, config: Option<&DeckConfig>, today: u32, v3: bool) -> Self {
|
||||
config
|
||||
.map(|config| {
|
||||
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;
|
||||
}
|
||||
RemainingLimits {
|
||||
review: ((config.inner.reviews_per_day as i32) - rev_today).max(0) as u32,
|
||||
new: ((config.inner.new_per_day as i32) - new_today).max(0) as u32,
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
if let Ok(normal) = deck.normal() {
|
||||
if let Some(config) = config {
|
||||
return Self::new_for_normal_deck(deck, today, v3, normal, config);
|
||||
}
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn new_for_normal_deck(
|
||||
deck: &Deck,
|
||||
today: u32,
|
||||
v3: bool,
|
||||
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) {
|
||||
|
|
|
@ -9,6 +9,7 @@ use serde_tuple::Serialize_tuple;
|
|||
|
||||
use super::{DeckCommon, FilteredDeck, FilteredSearchTerm, NormalDeck};
|
||||
use crate::{
|
||||
pb::decks::deck::normal::DayLimit,
|
||||
prelude::*,
|
||||
serde::{default_on_invalid, deserialize_bool_from_anything, deserialize_number_from_string},
|
||||
};
|
||||
|
@ -116,6 +117,14 @@ pub struct NormalDeckSchema11 {
|
|||
extend_new: i32,
|
||||
#[serde(default, deserialize_with = "default_on_invalid")]
|
||||
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)]
|
||||
|
@ -226,6 +235,10 @@ impl Default for NormalDeckSchema11 {
|
|||
conf: 1,
|
||||
extend_new: 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,
|
||||
markdown_description: deck.common.markdown_description,
|
||||
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,
|
||||
extend_new: norm.extend_new 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(),
|
||||
}),
|
||||
DeckKind::Filtered(ref filt) => DeckSchema11::Filtered(FilteredDeckSchema11 {
|
||||
|
@ -352,11 +373,12 @@ impl From<Deck> for DeckSchema11 {
|
|||
|
||||
impl From<Deck> for DeckCommonSchema11 {
|
||||
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()
|
||||
} else {
|
||||
serde_json::from_slice(&deck.common.other).unwrap_or_default()
|
||||
};
|
||||
clear_other_duplicates(&mut other);
|
||||
DeckCommonSchema11 {
|
||||
id: deck.id,
|
||||
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 {
|
||||
fn from(deck: &Deck) -> Self {
|
||||
let day = deck.common.last_day_studied as i32;
|
||||
|
|
|
@ -77,7 +77,7 @@ impl ExchangeData {
|
|||
|
||||
fn remove_scheduling_information(&mut self, col: &Collection) {
|
||||
self.remove_system_tags();
|
||||
self.reset_deck_config_ids();
|
||||
self.reset_deck_config_ids_and_limits();
|
||||
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() {
|
||||
if let Ok(normal_mut) = deck.normal_mut() {
|
||||
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 {
|
||||
// filtered decks are reset at import time for legacy reasons
|
||||
}
|
||||
|
|
|
@ -163,6 +163,10 @@ impl NormalDeck {
|
|||
if other.config_id != 1 {
|
||||
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>) {
|
||||
// see `clear_other_duplicates()` in `deckconfig/schema11.rs`
|
||||
for key in &["description"] {
|
||||
other.remove(*key);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import type Modal from "bootstrap/js/dist/modal";
|
||||
import { getContext } from "svelte";
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
|
||||
import ButtonGroup from "../components/ButtonGroup.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;
|
||||
const configList = state.configList;
|
||||
const dispatch = createEventDispatcher();
|
||||
const dispatchPresetChange = () => dispatch("presetchange");
|
||||
|
||||
function configLabel(entry: ConfigListEntry): string {
|
||||
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 {
|
||||
state.setCurrentIndex(parseInt((event.target! as HTMLSelectElement).value));
|
||||
dispatchPresetChange();
|
||||
}
|
||||
|
||||
function onAddConfig(text: string): void {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length) {
|
||||
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();
|
||||
if (trimmed.length) {
|
||||
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:clone={promptToClone}
|
||||
on:rename={promptToRename}
|
||||
on:remove={dispatchPresetChange}
|
||||
/>
|
||||
</ButtonToolbar>
|
||||
</StickyContainer>
|
||||
|
|
|
@ -7,14 +7,34 @@
|
|||
import Item from "../components/Item.svelte";
|
||||
import * as tr from "../lib/ftl";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
import { ValueTab } from "./lib";
|
||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||
import TabbedValue from "./TabbedValue.svelte";
|
||||
import TitledContainer from "./TitledContainer.svelte";
|
||||
import Warning from "./Warning.svelte";
|
||||
|
||||
export let state: DeckOptionsState;
|
||||
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 limits = state.deckLimits;
|
||||
const defaults = state.defaults;
|
||||
const parentLimits = state.parentLimits;
|
||||
|
||||
|
@ -24,28 +44,92 @@
|
|||
"\n\n" +
|
||||
tr.deckConfigLimitInterdayBoundByReviews() +
|
||||
"\n\n" +
|
||||
tr.deckConfigLimitDeckV3()
|
||||
tr.deckConfigLimitDeckV3() +
|
||||
"\n\n" +
|
||||
tr.deckConfigTabDescription()
|
||||
: "";
|
||||
|
||||
$: newCardsGreaterThanParent =
|
||||
!state.v3Scheduler && $config.newPerDay > $parentLimits.newCards
|
||||
!state.v3Scheduler && newValue > $parentLimits.newCards
|
||||
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
|
||||
: "";
|
||||
|
||||
$: reviewsTooLow =
|
||||
Math.min(9999, $config.newPerDay * 10) > $config.reviewsPerDay
|
||||
Math.min(9999, newValue * 10) > reviewsValue
|
||||
? tr.deckConfigReviewsTooLow({
|
||||
cards: $config.newPerDay,
|
||||
expected: Math.min(9999, $config.newPerDay * 10),
|
||||
cards: newValue,
|
||||
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>
|
||||
|
||||
<TitledContainer title={tr.deckConfigDailyLimits()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<TabbedValue tabs={newTabs} bind:value={newValue} />
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.newPerDay}
|
||||
bind:value={newValue}
|
||||
defaultValue={defaults.newPerDay}
|
||||
markdownTooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
|
||||
>
|
||||
|
@ -57,9 +141,10 @@
|
|||
<Warning warning={newCardsGreaterThanParent} />
|
||||
</Item>
|
||||
|
||||
<TabbedValue tabs={reviewTabs} bind:value={reviewsValue} />
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.reviewsPerDay}
|
||||
bind:value={reviewsValue}
|
||||
defaultValue={defaults.reviewsPerDay}
|
||||
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 audioOptions = {};
|
||||
export const advancedOptions = {};
|
||||
|
||||
let onPresetChange: () => void;
|
||||
</script>
|
||||
|
||||
<ConfigSelector {state} />
|
||||
<ConfigSelector {state} on:presetchange={onPresetChange} />
|
||||
|
||||
<div class="deck-options-page">
|
||||
<Container
|
||||
|
@ -68,7 +70,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<DynamicallySlottable slotHost={Item} api={options}>
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<DailyLimits {state} api={dailyLimits} />
|
||||
<DailyLimits {state} api={dailyLimits} bind:onPresetChange />
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
if (confirm(withCollapsedWhitespace(msg))) {
|
||||
try {
|
||||
state.removeCurrentConfig();
|
||||
dispatch("remove");
|
||||
} catch (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 cardStateCustomizer: Writable<string>;
|
||||
readonly currentDeck: DeckConfig.DeckConfigsForUpdate.CurrentDeck;
|
||||
readonly deckLimits: Writable<DeckConfig.DeckConfigsForUpdate.CurrentDeck.Limits>;
|
||||
readonly defaults: DeckConfig.DeckConfig.Config;
|
||||
readonly addonComponents: Writable<DynamicSvelteComponent[]>;
|
||||
readonly v3Scheduler: boolean;
|
||||
|
@ -68,6 +69,7 @@ export class DeckOptionsState {
|
|||
this.v3Scheduler = data.v3Scheduler;
|
||||
this.haveAddons = data.haveAddons;
|
||||
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
|
||||
// selected one at display time
|
||||
|
@ -190,6 +192,7 @@ export class DeckOptionsState {
|
|||
configs,
|
||||
applyToChildren,
|
||||
cardStateCustomizer: get(this.cardStateCustomizer),
|
||||
limits: get(this.deckLimits),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -309,3 +312,48 @@ function bytesToObject(bytes: Uint8Array): Record<string, unknown> {
|
|||
|
||||
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