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 commit 4ee7be1e10.

* Revert "Add DeckLimitsDialog"

This reverts commit eb0e2a62d3.

* 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 commit 0775814989.

* Add Tabs component for daily limits

* Add borders to tabs component

* Revert "Add borders to tabs component"

This reverts commit aaaf553893.

* 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:
RumovZ 2022-07-19 10:27:25 +02:00 committed by GitHub
parent e0368a3858
commit cc929687ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 432 additions and 31 deletions

View file

@ -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

View file

@ -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;
}

View file

@ -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 {

View file

@ -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():

View file

@ -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(

View file

@ -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(),
}
}
}

View file

@ -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());

View file

@ -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| {
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;
}
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,
Self {
review: (review_limit as i32 - rev_today).max(0) as u32,
new: (new_limit as i32 - new_today).max(0) as u32,
}
})
.unwrap_or_default()
}
pub(crate) fn cap_to(&mut self, limits: RemainingLimits) {

View file

@ -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;

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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>

View file

@ -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}
>

View file

@ -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>

View file

@ -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);
}

View 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>

View file

@ -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);
}
}