Convert FSRS to a global option

Allowing some decks to be FSRS and some SM-2 will lead to confusing
behavior when sorting on SM-2 or FSRS-specific fields, or when moving
cards between decks.
This commit is contained in:
Damien Elmes 2023-09-23 14:41:28 +10:00
parent 0071094e6c
commit c78de23cf9
11 changed files with 27 additions and 24 deletions

View file

@ -135,7 +135,6 @@ message DeckConfig {
bool bury_reviews = 28; bool bury_reviews = 28;
bool bury_interday_learning = 29; bool bury_interday_learning = 29;
bool fsrs_enabled = 36;
float desired_retention = 37; // for fsrs float desired_retention = 37; // for fsrs
bytes other = 255; bytes other = 255;
@ -179,6 +178,7 @@ message DeckConfigsForUpdate {
string card_state_customizer = 6; string card_state_customizer = 6;
// only applies to v3 scheduler // only applies to v3 scheduler
bool new_cards_ignore_review_limit = 7; bool new_cards_ignore_review_limit = 7;
bool fsrs = 8;
} }
message UpdateDeckConfigsRequest { message UpdateDeckConfigsRequest {
@ -191,4 +191,5 @@ message UpdateDeckConfigsRequest {
string card_state_customizer = 5; string card_state_customizer = 5;
DeckConfigsForUpdate.CurrentDeck.Limits limits = 6; DeckConfigsForUpdate.CurrentDeck.Limits limits = 6;
bool new_cards_ignore_review_limit = 7; bool new_cards_ignore_review_limit = 7;
bool fsrs = 8;
} }

View file

@ -37,6 +37,7 @@ pub enum BoolKey {
MergeNotetypes, MergeNotetypes,
WithScheduling, WithScheduling,
StopTimerOnAnswer, StopTimerOnAnswer,
Fsrs,
#[strum(to_string = "normalize_note_text")] #[strum(to_string = "normalize_note_text")]
NormalizeNoteText, NormalizeNoteText,

View file

@ -66,7 +66,6 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
bury_new: false, bury_new: false,
bury_reviews: false, bury_reviews: false,
bury_interday_learning: false, bury_interday_learning: false,
fsrs_enabled: false,
fsrs_weights: vec![], fsrs_weights: vec![],
desired_retention: 0.9, desired_retention: 0.9,
other: Vec::new(), other: Vec::new(),

View file

@ -68,8 +68,6 @@ pub struct DeckConfSchema11 {
#[serde(default)] #[serde(default)]
fsrs_weights: Vec<f32>, fsrs_weights: Vec<f32>,
#[serde(default)] #[serde(default)]
fsrs_enabled: bool,
#[serde(default)]
desired_retention: f32, desired_retention: f32,
#[serde(flatten)] #[serde(flatten)]
@ -258,7 +256,6 @@ impl Default for DeckConfSchema11 {
new_gather_priority: 0, new_gather_priority: 0,
bury_interday_learning: false, bury_interday_learning: false,
fsrs_weights: vec![], fsrs_weights: vec![],
fsrs_enabled: false,
desired_retention: 0.9, desired_retention: 0.9,
} }
} }
@ -329,7 +326,6 @@ impl From<DeckConfSchema11> for DeckConfig {
bury_reviews: c.rev.bury, bury_reviews: c.rev.bury,
bury_interday_learning: c.bury_interday_learning, bury_interday_learning: c.bury_interday_learning,
fsrs_weights: c.fsrs_weights, fsrs_weights: c.fsrs_weights,
fsrs_enabled: c.fsrs_enabled,
desired_retention: c.desired_retention, desired_retention: c.desired_retention,
other: other_bytes, other: other_bytes,
}, },
@ -423,7 +419,6 @@ impl From<DeckConfig> for DeckConfSchema11 {
new_gather_priority: i.new_card_gather_priority, new_gather_priority: i.new_card_gather_priority,
bury_interday_learning: i.bury_interday_learning, bury_interday_learning: i.bury_interday_learning,
fsrs_weights: i.fsrs_weights, fsrs_weights: i.fsrs_weights,
fsrs_enabled: i.fsrs_enabled,
desired_retention: i.desired_retention, desired_retention: i.desired_retention,
} }
} }
@ -448,7 +443,6 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
"newGatherPriority", "newGatherPriority",
"fsrsWeights", "fsrsWeights",
"desiredRetention", "desiredRetention",
"fsrsEnabled",
}; };
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! { static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {

View file

@ -102,6 +102,7 @@ impl From<anki_proto::deck_config::UpdateDeckConfigsRequest> for UpdateDeckConfi
card_state_customizer: c.card_state_customizer, card_state_customizer: c.card_state_customizer,
limits: c.limits.unwrap_or_default(), limits: c.limits.unwrap_or_default(),
new_cards_ignore_review_limit: c.new_cards_ignore_review_limit, new_cards_ignore_review_limit: c.new_cards_ignore_review_limit,
fsrs: c.fsrs,
} }
} }
} }

View file

@ -29,6 +29,7 @@ pub struct UpdateDeckConfigsRequest {
pub card_state_customizer: String, pub card_state_customizer: String,
pub limits: Limits, pub limits: Limits,
pub new_cards_ignore_review_limit: bool, pub new_cards_ignore_review_limit: bool,
pub fsrs: bool,
} }
impl Collection { impl Collection {
@ -48,6 +49,7 @@ impl Collection {
v3_scheduler: self.get_config_bool(BoolKey::Sched2021), v3_scheduler: self.get_config_bool(BoolKey::Sched2021),
card_state_customizer: self.get_config_string(StringKey::CardStateCustomizer), card_state_customizer: self.get_config_string(StringKey::CardStateCustomizer),
new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit), new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit),
fsrs: self.get_config_bool(BoolKey::Fsrs),
}) })
} }
@ -161,6 +163,10 @@ impl Collection {
let selected_config = input.configs.last().unwrap(); let selected_config = input.configs.last().unwrap();
let mut decks_needing_memory_recompute: HashMap<DeckConfigId, Vec<SearchNode>> = let mut decks_needing_memory_recompute: HashMap<DeckConfigId, Vec<SearchNode>> =
Default::default(); Default::default();
let fsrs_toggled = self.get_config_bool(BoolKey::Fsrs) != input.fsrs;
if fsrs_toggled {
self.set_config_bool_inner(BoolKey::Fsrs, input.fsrs)?;
}
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() {
let deck_id = deck.id; let deck_id = deck.id;
@ -171,9 +177,6 @@ impl Collection {
let previous_order = previous_config let previous_order = previous_config
.map(|c| c.inner.new_card_insert_order()) .map(|c| c.inner.new_card_insert_order())
.unwrap_or_default(); .unwrap_or_default();
let previous_fsrs_on = previous_config
.map(|c| c.inner.fsrs_enabled)
.unwrap_or_default();
let previous_weights = previous_config.map(|c| &c.inner.fsrs_weights); let previous_weights = previous_config.map(|c| &c.inner.fsrs_weights);
// if a selected (sub)deck, or its old config was removed, update deck to point // if a selected (sub)deck, or its old config was removed, update deck to point
@ -200,11 +203,8 @@ impl Collection {
} }
// if weights differ, memory state needs to be recomputed // if weights differ, memory state needs to be recomputed
let current_fsrs_on = current_config
.map(|c| c.inner.fsrs_enabled)
.unwrap_or_default();
let current_weights = current_config.map(|c| &c.inner.fsrs_weights); let current_weights = current_config.map(|c| &c.inner.fsrs_weights);
if current_fsrs_on != previous_fsrs_on || previous_weights != current_weights { if fsrs_toggled || previous_weights != current_weights {
decks_needing_memory_recompute decks_needing_memory_recompute
.entry(current_config_id) .entry(current_config_id)
.or_default() .or_default()
@ -220,7 +220,7 @@ impl Collection {
.into_iter() .into_iter()
.map(|(conf_id, search)| { .map(|(conf_id, search)| {
let weights = configs_after_update.get(&conf_id).and_then(|c| { let weights = configs_after_update.get(&conf_id).and_then(|c| {
if c.inner.fsrs_enabled { if input.fsrs {
Some(c.inner.fsrs_weights.clone()) Some(c.inner.fsrs_weights.clone())
} else { } else {
None None
@ -365,6 +365,7 @@ mod test {
card_state_customizer: "".to_string(), card_state_customizer: "".to_string(),
limits: Limits::default(), limits: Limits::default(),
new_cards_ignore_review_limit: false, new_cards_ignore_review_limit: false,
fsrs: false,
}; };
assert!(!col.update_deck_configs(input.clone())?.changes.had_change()); assert!(!col.update_deck_configs(input.clone())?.changes.had_change());

View file

@ -351,7 +351,7 @@ impl Collection {
.get_deck(card.deck_id)? .get_deck(card.deck_id)?
.or_not_found(card.deck_id)?; .or_not_found(card.deck_id)?;
let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?; let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?;
let fsrs_next_states = if config.inner.fsrs_enabled { let fsrs_next_states = if self.get_config_bool(BoolKey::Fsrs) {
let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?; let fsrs = FSRS::new(Some(&config.inner.fsrs_weights))?;
let memory_state = if let Some(state) = card.memory_state { let memory_state = if let Some(state) = card.memory_state {
Some(MemoryState::from(state)) Some(MemoryState::from(state))

View file

@ -28,6 +28,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const config = state.currentConfig; const config = state.currentConfig;
const defaults = state.defaults; const defaults = state.defaults;
const cardStateCustomizer = state.cardStateCustomizer; const cardStateCustomizer = state.cardStateCustomizer;
const fsrs = state.fsrs;
const settings = { const settings = {
maximumInterval: { maximumInterval: {
@ -76,7 +77,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
carousel.to(index); carousel.to(index);
} }
$: fsrsClientWarning = $config.fsrsEnabled ? tr.deckConfigFsrsOnAllClients() : ""; $: fsrsClientWarning = $fsrs ? tr.deckConfigFsrsOnAllClients() : "";
</script> </script>
<TitledContainer title={tr.deckConfigAdvancedTitle()}> <TitledContainer title={tr.deckConfigAdvancedTitle()}>
@ -93,7 +94,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<DynamicallySlottable slotHost={Item} {api}> <DynamicallySlottable slotHost={Item} {api}>
{#if state.v3Scheduler} {#if state.v3Scheduler}
<Item> <Item>
<SwitchRow bind:value={$config.fsrsEnabled} defaultValue={false}> <SwitchRow bind:value={$fsrs} defaultValue={false}>
<SettingTitle>FSRS</SettingTitle> <SettingTitle>FSRS</SettingTitle>
</SwitchRow> </SwitchRow>
</Item> </Item>
@ -117,7 +118,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SpinBoxRow> </SpinBoxRow>
</Item> </Item>
{#if !$config.fsrsEnabled || !state.v3Scheduler} {#if !$fsrs || !state.v3Scheduler}
<Item> <Item>
<SpinBoxFloatRow <SpinBoxFloatRow
bind:value={$config.initialEase} bind:value={$config.initialEase}

View file

@ -26,6 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const config = state.currentConfig; const config = state.currentConfig;
const defaults = state.defaults; const defaults = state.defaults;
const fsrs = state.fsrs;
let stepsExceedMinimumInterval: string; let stepsExceedMinimumInterval: string;
let stepsTooLargeForFsrs: string; let stepsTooLargeForFsrs: string;
@ -38,7 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
? tr.deckConfigRelearningStepsAboveMinimumInterval() ? tr.deckConfigRelearningStepsAboveMinimumInterval()
: ""; : "";
stepsTooLargeForFsrs = stepsTooLargeForFsrs =
$config.fsrsEnabled && lastRelearnStepInDays >= 1 $fsrs && lastRelearnStepInDays >= 1
? tr.deckConfigStepsTooLargeForFsrs() ? tr.deckConfigStepsTooLargeForFsrs()
: ""; : "";
} }
@ -106,7 +107,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<Warning warning={stepsTooLargeForFsrs} /> <Warning warning={stepsTooLargeForFsrs} />
</Item> </Item>
{#if !$config.fsrsEnabled} {#if !$fsrs}
<Item> <Item>
<SpinBoxRow <SpinBoxRow
bind:value={$config.minimumLapseInterval} bind:value={$config.minimumLapseInterval}

View file

@ -27,6 +27,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const config = state.currentConfig; const config = state.currentConfig;
const defaults = state.defaults; const defaults = state.defaults;
const fsrs = state.fsrs;
let stepsExceedGraduatingInterval: string; let stepsExceedGraduatingInterval: string;
let stepsTooLargeForFsrs: string; let stepsTooLargeForFsrs: string;
@ -39,7 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
? tr.deckConfigLearningStepAboveGraduatingInterval() ? tr.deckConfigLearningStepAboveGraduatingInterval()
: ""; : "";
stepsTooLargeForFsrs = stepsTooLargeForFsrs =
$config.fsrsEnabled && lastLearnStepInDays >= 1 $fsrs && lastLearnStepInDays >= 1
? tr.deckConfigStepsTooLargeForFsrs() ? tr.deckConfigStepsTooLargeForFsrs()
: ""; : "";
} }
@ -118,7 +119,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<Warning warning={stepsTooLargeForFsrs} /> <Warning warning={stepsTooLargeForFsrs} />
</Item> </Item>
{#if !$config.fsrsEnabled} {#if !$fsrs}
<Item> <Item>
<SpinBoxRow <SpinBoxRow
bind:value={$config.graduatingIntervalGood} bind:value={$config.graduatingIntervalGood}

View file

@ -48,6 +48,7 @@ export class DeckOptionsState {
readonly addonComponents: Writable<DynamicSvelteComponent[]>; readonly addonComponents: Writable<DynamicSvelteComponent[]>;
readonly v3Scheduler: boolean; readonly v3Scheduler: boolean;
readonly newCardsIgnoreReviewLimit: Writable<boolean>; readonly newCardsIgnoreReviewLimit: Writable<boolean>;
readonly fsrs: Writable<boolean>;
private targetDeckId: DeckOptionsId; private targetDeckId: DeckOptionsId;
private configs: ConfigWithCount[]; private configs: ConfigWithCount[];
@ -79,6 +80,7 @@ export class DeckOptionsState {
this.cardStateCustomizer = writable(data.cardStateCustomizer); this.cardStateCustomizer = writable(data.cardStateCustomizer);
this.deckLimits = writable(data.currentDeck?.limits ?? createLimits()); this.deckLimits = writable(data.currentDeck?.limits ?? createLimits());
this.newCardsIgnoreReviewLimit = writable(data.newCardsIgnoreReviewLimit); this.newCardsIgnoreReviewLimit = writable(data.newCardsIgnoreReviewLimit);
this.fsrs = writable(data.fsrs);
// 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
@ -205,6 +207,7 @@ export class DeckOptionsState {
cardStateCustomizer: get(this.cardStateCustomizer), cardStateCustomizer: get(this.cardStateCustomizer),
limits: get(this.deckLimits), limits: get(this.deckLimits),
newCardsIgnoreReviewLimit: get(this.newCardsIgnoreReviewLimit), newCardsIgnoreReviewLimit: get(this.newCardsIgnoreReviewLimit),
fsrs: get(this.fsrs),
}; };
} }