mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Add option to calculate all weights at once
This commit is contained in:
parent
c67f510b9a
commit
452e012c71
14 changed files with 162 additions and 46 deletions
|
@ -293,6 +293,7 @@ deck-config-confirm-remove-name = Remove { $name }?
|
||||||
|
|
||||||
deck-config-save-button = Save
|
deck-config-save-button = Save
|
||||||
deck-config-save-to-all-subdecks = Save to All Subdecks
|
deck-config-save-to-all-subdecks = Save to All Subdecks
|
||||||
|
deck-config-save-and-optimize = Optimize All Presets
|
||||||
deck-config-revert-button-tooltip = Restore this setting to its default value.
|
deck-config-revert-button-tooltip = Restore this setting to its default value.
|
||||||
|
|
||||||
## These strings are shown via the Description button at the bottom of the
|
## These strings are shown via the Description button at the bottom of the
|
||||||
|
@ -409,6 +410,13 @@ deck-config-a-100-day-interval =
|
||||||
[one] A 100 day interval will become { $days } day.
|
[one] A 100 day interval will become { $days } day.
|
||||||
*[other] A 100 day interval will become { $days } days.
|
*[other] A 100 day interval will become { $days } days.
|
||||||
}
|
}
|
||||||
|
deck-config-percent-of-reviews =
|
||||||
|
{ $reviews ->
|
||||||
|
[one] { $pct }% of { $reviews } review
|
||||||
|
*[other] { $pct }% of { $reviews } reviews
|
||||||
|
}
|
||||||
|
deck-config-optimizing-preset = Optimizing preset { $current_count }/{ $total_count }...
|
||||||
|
deck-config-fsrs-must-be-enabled = FSRS must be enabled first.
|
||||||
|
|
||||||
deck-config-wait-for-audio = Wait for audio
|
deck-config-wait-for-audio = Wait for audio
|
||||||
deck-config-show-reminder = Show Reminder
|
deck-config-show-reminder = Show Reminder
|
||||||
|
|
|
@ -134,9 +134,15 @@ message Progress {
|
||||||
}
|
}
|
||||||
|
|
||||||
message ComputeWeightsProgress {
|
message ComputeWeightsProgress {
|
||||||
|
// Current iteration
|
||||||
uint32 current = 1;
|
uint32 current = 1;
|
||||||
|
// Total iterations
|
||||||
uint32 total = 2;
|
uint32 total = 2;
|
||||||
uint32 fsrs_items = 3;
|
uint32 fsrs_items = 3;
|
||||||
|
// Only used in 'compute all weights' case
|
||||||
|
uint32 current_preset = 4;
|
||||||
|
// Only used in 'compute all weights' case
|
||||||
|
uint32 total_presets = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ComputeRetentionProgress {
|
message ComputeRetentionProgress {
|
||||||
|
|
|
@ -200,13 +200,19 @@ message DeckConfigsForUpdate {
|
||||||
bool apply_all_parent_limits = 9;
|
bool apply_all_parent_limits = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UpdateDeckConfigsMode {
|
||||||
|
UPDATE_DECK_CONFIGS_MODE_NORMAL = 0;
|
||||||
|
UPDATE_DECK_CONFIGS_MODE_APPLY_TO_CHILDREN = 1;
|
||||||
|
UPDATE_DECK_CONFIGS_MODE_COMPUTE_ALL_WEIGHTS = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message UpdateDeckConfigsRequest {
|
message UpdateDeckConfigsRequest {
|
||||||
int64 target_deck_id = 1;
|
int64 target_deck_id = 1;
|
||||||
/// Unchanged, non-selected configs can be omitted. Deck will
|
/// Unchanged, non-selected configs can be omitted. Deck will
|
||||||
/// be set to whichever entry comes last.
|
/// be set to whichever entry comes last.
|
||||||
repeated DeckConfig configs = 2;
|
repeated DeckConfig configs = 2;
|
||||||
repeated int64 removed_config_ids = 3;
|
repeated int64 removed_config_ids = 3;
|
||||||
bool apply_to_children = 4;
|
UpdateDeckConfigsMode mode = 4;
|
||||||
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;
|
||||||
|
|
|
@ -36,7 +36,7 @@ from aqt.operations import on_op_finished
|
||||||
from aqt.operations.deck import update_deck_configs as update_deck_configs_op
|
from aqt.operations.deck import update_deck_configs as update_deck_configs_op
|
||||||
from aqt.progress import ProgressUpdate
|
from aqt.progress import ProgressUpdate
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import aqt_data_path, show_warning
|
from aqt.utils import aqt_data_path, show_warning, tr
|
||||||
|
|
||||||
app = flask.Flask(__name__, root_path="/fake")
|
app = flask.Flask(__name__, root_path="/fake")
|
||||||
flask_cors.CORS(app, resources={r"/*": {"origins": "127.0.0.1"}})
|
flask_cors.CORS(app, resources={r"/*": {"origins": "127.0.0.1"}})
|
||||||
|
@ -433,12 +433,26 @@ def update_deck_configs() -> bytes:
|
||||||
input.ParseFromString(request.data)
|
input.ParseFromString(request.data)
|
||||||
|
|
||||||
def on_progress(progress: Progress, update: ProgressUpdate) -> None:
|
def on_progress(progress: Progress, update: ProgressUpdate) -> None:
|
||||||
if not progress.HasField("compute_memory"):
|
if progress.HasField("compute_memory"):
|
||||||
|
val = progress.compute_memory
|
||||||
|
update.max = val.total_cards
|
||||||
|
update.value = val.current_cards
|
||||||
|
update.label = val.label
|
||||||
|
elif progress.HasField("compute_weights"):
|
||||||
|
val2 = progress.compute_weights
|
||||||
|
update.max = val2.total
|
||||||
|
update.value = val2.current
|
||||||
|
pct = str(int(val2.current / val2.total * 100) if val2.total > 0 else 0)
|
||||||
|
label = tr.deck_config_optimizing_preset(
|
||||||
|
current_count=val2.current_preset, total_count=val2.total_presets
|
||||||
|
)
|
||||||
|
update.label = (
|
||||||
|
label
|
||||||
|
+ "\n"
|
||||||
|
+ tr.deck_config_percent_of_reviews(pct=pct, reviews=val2.fsrs_items)
|
||||||
|
)
|
||||||
|
else:
|
||||||
return
|
return
|
||||||
val = progress.compute_memory
|
|
||||||
update.max = val.total_cards
|
|
||||||
update.value = val.current_cards
|
|
||||||
update.label = val.label
|
|
||||||
if update.user_wants_abort:
|
if update.user_wants_abort:
|
||||||
update.abort = True
|
update.abort = True
|
||||||
|
|
||||||
|
|
|
@ -94,11 +94,12 @@ impl From<DeckConfig> for anki_proto::deck_config::DeckConfig {
|
||||||
|
|
||||||
impl From<anki_proto::deck_config::UpdateDeckConfigsRequest> for UpdateDeckConfigsRequest {
|
impl From<anki_proto::deck_config::UpdateDeckConfigsRequest> for UpdateDeckConfigsRequest {
|
||||||
fn from(c: anki_proto::deck_config::UpdateDeckConfigsRequest) -> Self {
|
fn from(c: anki_proto::deck_config::UpdateDeckConfigsRequest) -> Self {
|
||||||
|
let mode = c.mode();
|
||||||
UpdateDeckConfigsRequest {
|
UpdateDeckConfigsRequest {
|
||||||
target_deck_id: c.target_deck_id.into(),
|
target_deck_id: c.target_deck_id.into(),
|
||||||
configs: c.configs.into_iter().map(Into::into).collect(),
|
configs: c.configs.into_iter().map(Into::into).collect(),
|
||||||
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,
|
mode,
|
||||||
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,
|
||||||
|
|
|
@ -10,6 +10,7 @@ use std::iter;
|
||||||
use anki_proto::deck_config::deck_configs_for_update::current_deck::Limits;
|
use anki_proto::deck_config::deck_configs_for_update::current_deck::Limits;
|
||||||
use anki_proto::deck_config::deck_configs_for_update::ConfigWithExtra;
|
use anki_proto::deck_config::deck_configs_for_update::ConfigWithExtra;
|
||||||
use anki_proto::deck_config::deck_configs_for_update::CurrentDeck;
|
use anki_proto::deck_config::deck_configs_for_update::CurrentDeck;
|
||||||
|
use anki_proto::deck_config::UpdateDeckConfigsMode;
|
||||||
use anki_proto::decks::deck::normal::DayLimit;
|
use anki_proto::decks::deck::normal::DayLimit;
|
||||||
use fsrs::DEFAULT_WEIGHTS;
|
use fsrs::DEFAULT_WEIGHTS;
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ pub struct UpdateDeckConfigsRequest {
|
||||||
/// Deck will be set to last provided deck config.
|
/// Deck will be set to last provided deck config.
|
||||||
pub configs: Vec<DeckConfig>,
|
pub configs: Vec<DeckConfig>,
|
||||||
pub removed_config_ids: Vec<DeckConfigId>,
|
pub removed_config_ids: Vec<DeckConfigId>,
|
||||||
pub apply_to_children: bool,
|
pub mode: UpdateDeckConfigsMode,
|
||||||
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,
|
||||||
|
@ -136,6 +137,10 @@ impl Collection {
|
||||||
configs_after_update.remove(dcid);
|
configs_after_update.remove(dcid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.mode == UpdateDeckConfigsMode::ComputeAllWeights {
|
||||||
|
self.compute_all_weights(&mut req)?;
|
||||||
|
}
|
||||||
|
|
||||||
// add/update provided configs
|
// add/update provided configs
|
||||||
for conf in &mut req.configs {
|
for conf in &mut req.configs {
|
||||||
let weight_len = conf.inner.fsrs_weights.len();
|
let weight_len = conf.inner.fsrs_weights.len();
|
||||||
|
@ -147,7 +152,7 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get selected deck and possibly children
|
// get selected deck and possibly children
|
||||||
let selected_deck_ids: HashSet<_> = if req.apply_to_children {
|
let selected_deck_ids: HashSet<_> = if req.mode == UpdateDeckConfigsMode::ApplyToChildren {
|
||||||
let deck = self
|
let deck = self
|
||||||
.storage
|
.storage
|
||||||
.get_deck(req.target_deck_id)?
|
.get_deck(req.target_deck_id)?
|
||||||
|
@ -295,6 +300,46 @@ impl Collection {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
fn compute_all_weights(&mut self, req: &mut UpdateDeckConfigsRequest) -> Result<()> {
|
||||||
|
require!(req.fsrs, "FSRS must be enabled");
|
||||||
|
|
||||||
|
// frontend didn't include any unmodified deck configs, so we need to fill them
|
||||||
|
// in
|
||||||
|
let changed_configs: HashSet<_> = req.configs.iter().map(|c| c.id).collect();
|
||||||
|
let previous_last = req.configs.pop().or_invalid("no configs provided")?;
|
||||||
|
for config in self.storage.all_deck_config()? {
|
||||||
|
if !changed_configs.contains(&config.id) {
|
||||||
|
req.configs.push(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// other parts of the code expect the currently-selected preset to come last
|
||||||
|
req.configs.push(previous_last);
|
||||||
|
|
||||||
|
// calculate and apply weights to each preset
|
||||||
|
let config_len = req.configs.len() as u32;
|
||||||
|
for (idx, config) in req.configs.iter_mut().enumerate() {
|
||||||
|
let search = if config.inner.weight_search.trim().is_empty() {
|
||||||
|
SearchNode::Preset(config.name.clone())
|
||||||
|
.try_into_search()?
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
config.inner.weight_search.clone()
|
||||||
|
};
|
||||||
|
match self.compute_weights(&search, idx as u32 + 1, config_len) {
|
||||||
|
Ok(weights) => {
|
||||||
|
if weights.fsrs_items >= 1000 {
|
||||||
|
println!("{}: {:?}", config.name, weights.weights);
|
||||||
|
config.inner.fsrs_weights = weights.weights;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(AnkiError::Interrupted) => return Err(AnkiError::Interrupted),
|
||||||
|
Err(err) => {
|
||||||
|
println!("{}: {}", config.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normal_deck_to_limits(deck: &NormalDeck, today: u32) -> Limits {
|
fn normal_deck_to_limits(deck: &NormalDeck, today: u32) -> Limits {
|
||||||
|
@ -383,7 +428,7 @@ mod test {
|
||||||
.map(|c| c.config.unwrap().into())
|
.map(|c| c.config.unwrap().into())
|
||||||
.collect(),
|
.collect(),
|
||||||
removed_config_ids: vec![],
|
removed_config_ids: vec![],
|
||||||
apply_to_children: false,
|
mode: UpdateDeckConfigsMode::Normal,
|
||||||
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,
|
||||||
|
|
|
@ -211,9 +211,11 @@ pub(crate) fn progress_to_proto(
|
||||||
),
|
),
|
||||||
Progress::ComputeWeights(progress) => {
|
Progress::ComputeWeights(progress) => {
|
||||||
Value::ComputeWeights(anki_proto::collection::ComputeWeightsProgress {
|
Value::ComputeWeights(anki_proto::collection::ComputeWeightsProgress {
|
||||||
current: progress.current,
|
current: progress.current_iteration,
|
||||||
total: progress.total,
|
total: progress.total_iterations,
|
||||||
fsrs_items: progress.fsrs_items,
|
fsrs_items: progress.fsrs_items,
|
||||||
|
current_preset: progress.current_preset,
|
||||||
|
total_presets: progress.total_presets,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Progress::ComputeRetention(progress) => {
|
Progress::ComputeRetention(progress) => {
|
||||||
|
|
|
@ -27,13 +27,25 @@ use crate::search::SortMode;
|
||||||
pub(crate) type Weights = Vec<f32>;
|
pub(crate) type Weights = Vec<f32>;
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn compute_weights(&mut self, search: &str) -> Result<ComputeFsrsWeightsResponse> {
|
/// Note this does not return an error if there are less than 1000 items -
|
||||||
|
/// the caller should instead check the fsrs_items count in the return
|
||||||
|
/// value.
|
||||||
|
pub fn compute_weights(
|
||||||
|
&mut self,
|
||||||
|
search: &str,
|
||||||
|
current_preset: u32,
|
||||||
|
total_presets: u32,
|
||||||
|
) -> Result<ComputeFsrsWeightsResponse> {
|
||||||
let mut anki_progress = self.new_progress_handler::<ComputeWeightsProgress>();
|
let mut anki_progress = self.new_progress_handler::<ComputeWeightsProgress>();
|
||||||
let timing = self.timing_today()?;
|
let timing = self.timing_today()?;
|
||||||
let revlogs = self.revlog_for_srs(search)?;
|
let revlogs = self.revlog_for_srs(search)?;
|
||||||
let items = fsrs_items_for_training(revlogs, timing.next_day_at);
|
let items = fsrs_items_for_training(revlogs, timing.next_day_at);
|
||||||
let fsrs_items = items.len() as u32;
|
let fsrs_items = items.len() as u32;
|
||||||
anki_progress.update(false, |p| p.fsrs_items = fsrs_items)?;
|
anki_progress.update(false, |p| {
|
||||||
|
p.fsrs_items = fsrs_items;
|
||||||
|
p.current_preset = current_preset;
|
||||||
|
p.total_presets = total_presets;
|
||||||
|
})?;
|
||||||
// adapt the progress handler to our built-in progress handling
|
// adapt the progress handler to our built-in progress handling
|
||||||
let progress = CombinedProgressState::new_shared();
|
let progress = CombinedProgressState::new_shared();
|
||||||
let progress2 = progress.clone();
|
let progress2 = progress.clone();
|
||||||
|
@ -43,9 +55,9 @@ impl Collection {
|
||||||
thread::sleep(Duration::from_millis(100));
|
thread::sleep(Duration::from_millis(100));
|
||||||
let mut guard = progress.lock().unwrap();
|
let mut guard = progress.lock().unwrap();
|
||||||
if let Err(_err) = anki_progress.update(false, |s| {
|
if let Err(_err) = anki_progress.update(false, |s| {
|
||||||
s.total = guard.total() as u32;
|
s.total_iterations = guard.total() as u32;
|
||||||
s.current = guard.current() as u32;
|
s.current_iteration = guard.current() as u32;
|
||||||
finished = s.total > 0 && s.total == s.current;
|
finished = guard.finished();
|
||||||
}) {
|
}) {
|
||||||
guard.want_abort = true;
|
guard.want_abort = true;
|
||||||
return;
|
return;
|
||||||
|
@ -112,8 +124,8 @@ impl Collection {
|
||||||
Ok(fsrs.evaluate(items, |ip| {
|
Ok(fsrs.evaluate(items, |ip| {
|
||||||
anki_progress
|
anki_progress
|
||||||
.update(false, |p| {
|
.update(false, |p| {
|
||||||
p.total = ip.total as u32;
|
p.total_iterations = ip.total as u32;
|
||||||
p.current = ip.current as u32;
|
p.current_iteration = ip.current as u32;
|
||||||
})
|
})
|
||||||
.is_ok()
|
.is_ok()
|
||||||
})?)
|
})?)
|
||||||
|
@ -122,9 +134,13 @@ impl Collection {
|
||||||
|
|
||||||
#[derive(Default, Clone, Copy, Debug)]
|
#[derive(Default, Clone, Copy, Debug)]
|
||||||
pub struct ComputeWeightsProgress {
|
pub struct ComputeWeightsProgress {
|
||||||
pub current: u32,
|
pub current_iteration: u32,
|
||||||
pub total: u32,
|
pub total_iterations: u32,
|
||||||
pub fsrs_items: u32,
|
pub fsrs_items: u32,
|
||||||
|
/// Only used in 'compute all weights' case
|
||||||
|
pub current_preset: u32,
|
||||||
|
/// Only used in 'compute all weights' case
|
||||||
|
pub total_presets: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a series of revlog entries sorted by card id into FSRS items.
|
/// Convert a series of revlog entries sorted by card id into FSRS items.
|
||||||
|
|
|
@ -254,7 +254,7 @@ impl crate::services::SchedulerService for Collection {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: scheduler::ComputeFsrsWeightsRequest,
|
input: scheduler::ComputeFsrsWeightsRequest,
|
||||||
) -> Result<scheduler::ComputeFsrsWeightsResponse> {
|
) -> Result<scheduler::ComputeFsrsWeightsResponse> {
|
||||||
self.compute_weights(&input.search)
|
self.compute_weights(&input.search, 1, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_optimal_retention(
|
fn compute_optimal_retention(
|
||||||
|
|
|
@ -47,6 +47,12 @@ pub(super) fn write_nodes(nodes: &[Node]) -> String {
|
||||||
nodes.iter().map(write_node).collect()
|
nodes.iter().map(write_node).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToString for Node {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
write_node(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn write_node(node: &Node) -> String {
|
fn write_node(node: &Node) -> String {
|
||||||
use Node::*;
|
use Node::*;
|
||||||
match node {
|
match node {
|
||||||
|
|
|
@ -202,12 +202,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
if (!val || !val.total) {
|
if (!val || !val.total) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
let pct = ((val.current / val.total) * 100).toFixed(1);
|
const pct = ((val.current / val.total) * 100).toFixed(1);
|
||||||
pct = `${pct}%`;
|
|
||||||
if (val instanceof ComputeRetentionProgress) {
|
if (val instanceof ComputeRetentionProgress) {
|
||||||
return pct;
|
return `${pct}%`;
|
||||||
} else {
|
} else {
|
||||||
return `${pct} of ${val.fsrsItems} reviews`;
|
return tr.deckConfigPercentOfReviews({ pct, reviews: val.fsrsItems });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,12 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { UpdateDeckConfigsMode } from "@tslib/anki/deck_config_pb";
|
||||||
import * as tr from "@tslib/ftl";
|
import * as tr from "@tslib/ftl";
|
||||||
import { withCollapsedWhitespace } from "@tslib/i18n";
|
import { withCollapsedWhitespace } from "@tslib/i18n";
|
||||||
import { getPlatformString } from "@tslib/shortcuts";
|
import { getPlatformString } from "@tslib/shortcuts";
|
||||||
import { createEventDispatcher, tick } from "svelte";
|
import { createEventDispatcher, tick } from "svelte";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
import DropdownDivider from "../components/DropdownDivider.svelte";
|
import DropdownDivider from "../components/DropdownDivider.svelte";
|
||||||
import DropdownItem from "../components/DropdownItem.svelte";
|
import DropdownItem from "../components/DropdownItem.svelte";
|
||||||
|
@ -57,9 +59,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(applyToChildDecks: boolean): Promise<void> {
|
async function save(mode: UpdateDeckConfigsMode): Promise<void> {
|
||||||
await commitEditing();
|
await commitEditing();
|
||||||
state.save(applyToChildDecks);
|
if (!get(state.fsrs)) {
|
||||||
|
alert(tr.deckConfigFsrsMustBeEnabled());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.save(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveKeyCombination = "Control+Enter";
|
const saveKeyCombination = "Control+Enter";
|
||||||
|
@ -69,14 +75,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<LabelButton
|
<LabelButton
|
||||||
primary
|
primary
|
||||||
on:click={() => save(false)}
|
on:click={() => save(UpdateDeckConfigsMode.NORMAL)}
|
||||||
tooltip={getPlatformString(saveKeyCombination)}
|
tooltip={getPlatformString(saveKeyCombination)}
|
||||||
--border-left-radius={!rtl ? "var(--border-radius)" : "0"}
|
--border-left-radius={!rtl ? "var(--border-radius)" : "0"}
|
||||||
--border-right-radius={rtl ? "var(--border-radius)" : "0"}
|
--border-right-radius={rtl ? "var(--border-radius)" : "0"}
|
||||||
>
|
>
|
||||||
<div class="save">{tr.deckConfigSaveButton()}</div>
|
<div class="save">{tr.deckConfigSaveButton()}</div>
|
||||||
</LabelButton>
|
</LabelButton>
|
||||||
<Shortcut keyCombination={saveKeyCombination} on:action={() => save(false)} />
|
<Shortcut
|
||||||
|
keyCombination={saveKeyCombination}
|
||||||
|
on:action={() => save(UpdateDeckConfigsMode.NORMAL)}
|
||||||
|
/>
|
||||||
|
|
||||||
<WithFloating
|
<WithFloating
|
||||||
show={showFloating}
|
show={showFloating}
|
||||||
|
@ -108,9 +117,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
{tr.deckConfigRemoveGroup()}
|
{tr.deckConfigRemoveGroup()}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownDivider />
|
<DropdownDivider />
|
||||||
<DropdownItem on:click={() => save(true)}>
|
<DropdownItem on:click={() => save(UpdateDeckConfigsMode.APPLY_TO_CHILDREN)}>
|
||||||
{tr.deckConfigSaveToAllSubdecks()}
|
{tr.deckConfigSaveToAllSubdecks()}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<DropdownItem on:click={() => save(UpdateDeckConfigsMode.COMPUTE_ALL_WEIGHTS)}>
|
||||||
|
{tr.deckConfigSaveAndOptimize()}
|
||||||
|
</DropdownItem>
|
||||||
</Popover>
|
</Popover>
|
||||||
</WithFloating>
|
</WithFloating>
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { protoBase64 } from "@bufbuild/protobuf";
|
import { protoBase64 } from "@bufbuild/protobuf";
|
||||||
import { DeckConfig_Config_LeechAction, DeckConfigsForUpdate } from "@tslib/anki/deck_config_pb";
|
import { DeckConfig_Config_LeechAction, DeckConfigsForUpdate, UpdateDeckConfigsMode } from "@tslib/anki/deck_config_pb";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
import { DeckOptionsState } from "./lib";
|
import { DeckOptionsState } from "./lib";
|
||||||
|
@ -203,7 +203,7 @@ test("deck list", () => {
|
||||||
expect(get(state.currentConfig).newPerDay).toBe(10);
|
expect(get(state.currentConfig).newPerDay).toBe(10);
|
||||||
|
|
||||||
// only the pre-existing deck should be listed for removal
|
// only the pre-existing deck should be listed for removal
|
||||||
const out = state.dataForSaving(false);
|
const out = state.dataForSaving(UpdateDeckConfigsMode.NORMAL);
|
||||||
expect(out.removedConfigIds).toStrictEqual([1618570764780n]);
|
expect(out.removedConfigIds).toStrictEqual([1618570764780n]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -221,24 +221,24 @@ test("duplicate name", () => {
|
||||||
|
|
||||||
test("saving", () => {
|
test("saving", () => {
|
||||||
let state = startingState();
|
let state = startingState();
|
||||||
let out = state.dataForSaving(false);
|
let out = state.dataForSaving(UpdateDeckConfigsMode.NORMAL);
|
||||||
expect(out.removedConfigIds).toStrictEqual([]);
|
expect(out.removedConfigIds).toStrictEqual([]);
|
||||||
expect(out.targetDeckId).toBe(123n);
|
expect(out.targetDeckId).toBe(123n);
|
||||||
// in no-changes case, currently selected config should
|
// in no-changes case, currently selected config should
|
||||||
// be returned
|
// be returned
|
||||||
expect(out.configs!.length).toBe(1);
|
expect(out.configs!.length).toBe(1);
|
||||||
expect(out.configs![0].name).toBe("another one");
|
expect(out.configs![0].name).toBe("another one");
|
||||||
expect(out.applyToChildren).toBe(false);
|
expect(out.mode).toBe(UpdateDeckConfigsMode.NORMAL);
|
||||||
|
|
||||||
// rename, then change current deck
|
// rename, then change current deck
|
||||||
state.setCurrentName("zzz");
|
state.setCurrentName("zzz");
|
||||||
out = state.dataForSaving(true);
|
out = state.dataForSaving(UpdateDeckConfigsMode.APPLY_TO_CHILDREN);
|
||||||
state.setCurrentIndex(0);
|
state.setCurrentIndex(0);
|
||||||
|
|
||||||
// renamed deck should be in changes, with current deck as last element
|
// renamed deck should be in changes, with current deck as last element
|
||||||
out = state.dataForSaving(true);
|
out = state.dataForSaving(UpdateDeckConfigsMode.APPLY_TO_CHILDREN);
|
||||||
expect(out.configs!.map((c) => c.name)).toStrictEqual(["zzz", "Default"]);
|
expect(out.configs!.map((c) => c.name)).toStrictEqual(["zzz", "Default"]);
|
||||||
expect(out.applyToChildren).toBe(true);
|
expect(out.mode).toBe(UpdateDeckConfigsMode.APPLY_TO_CHILDREN);
|
||||||
|
|
||||||
// start again, adding new deck
|
// start again, adding new deck
|
||||||
state = startingState();
|
state = startingState();
|
||||||
|
@ -246,7 +246,7 @@ test("saving", () => {
|
||||||
|
|
||||||
// deleting it should not change removedConfigs
|
// deleting it should not change removedConfigs
|
||||||
state.removeCurrentConfig();
|
state.removeCurrentConfig();
|
||||||
out = state.dataForSaving(true);
|
out = state.dataForSaving(UpdateDeckConfigsMode.APPLY_TO_CHILDREN);
|
||||||
expect(out.removedConfigIds).toStrictEqual([]);
|
expect(out.removedConfigIds).toStrictEqual([]);
|
||||||
|
|
||||||
// select the other non-default deck & remove
|
// select the other non-default deck & remove
|
||||||
|
@ -255,7 +255,7 @@ test("saving", () => {
|
||||||
|
|
||||||
// should be listed in removedConfigs, and modified should
|
// should be listed in removedConfigs, and modified should
|
||||||
// only contain Default, which is the new current deck
|
// only contain Default, which is the new current deck
|
||||||
out = state.dataForSaving(true);
|
out = state.dataForSaving(UpdateDeckConfigsMode.APPLY_TO_CHILDREN);
|
||||||
expect(out.removedConfigIds).toStrictEqual([1618570764780n]);
|
expect(out.removedConfigIds).toStrictEqual([1618570764780n]);
|
||||||
expect(out.configs!.map((c) => c.name)).toStrictEqual(["Default"]);
|
expect(out.configs!.map((c) => c.name)).toStrictEqual(["Default"]);
|
||||||
});
|
});
|
||||||
|
@ -283,7 +283,7 @@ test("aux data", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ensure changes serialize
|
// ensure changes serialize
|
||||||
const out = state.dataForSaving(true);
|
const out = state.dataForSaving(UpdateDeckConfigsMode.APPLY_TO_CHILDREN);
|
||||||
expect(out.configs!.length).toBe(2);
|
expect(out.configs!.length).toBe(2);
|
||||||
const json = out.configs!.map((c) => JSON.parse(new TextDecoder().decode(c.config!.other)));
|
const json = out.configs!.map((c) => JSON.parse(new TextDecoder().decode(c.config!.other)));
|
||||||
expect(json).toStrictEqual([
|
expect(json).toStrictEqual([
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { PlainMessage } from "@bufbuild/protobuf";
|
||||||
import type {
|
import type {
|
||||||
DeckConfigsForUpdate,
|
DeckConfigsForUpdate,
|
||||||
DeckConfigsForUpdate_CurrentDeck,
|
DeckConfigsForUpdate_CurrentDeck,
|
||||||
|
UpdateDeckConfigsMode,
|
||||||
UpdateDeckConfigsRequest,
|
UpdateDeckConfigsRequest,
|
||||||
} from "@tslib/anki/deck_config_pb";
|
} from "@tslib/anki/deck_config_pb";
|
||||||
import { DeckConfig, DeckConfig_Config, DeckConfigsForUpdate_CurrentDeck_Limits } from "@tslib/anki/deck_config_pb";
|
import { DeckConfig, DeckConfig_Config, DeckConfigsForUpdate_CurrentDeck_Limits } from "@tslib/anki/deck_config_pb";
|
||||||
|
@ -178,7 +179,7 @@ export class DeckOptionsState {
|
||||||
}
|
}
|
||||||
|
|
||||||
dataForSaving(
|
dataForSaving(
|
||||||
applyToChildren: boolean,
|
mode: UpdateDeckConfigsMode,
|
||||||
): PlainMessage<UpdateDeckConfigsRequest> {
|
): PlainMessage<UpdateDeckConfigsRequest> {
|
||||||
const modifiedConfigsExcludingCurrent = this.configs
|
const modifiedConfigsExcludingCurrent = this.configs
|
||||||
.map((c) => c.config)
|
.map((c) => c.config)
|
||||||
|
@ -197,7 +198,7 @@ export class DeckOptionsState {
|
||||||
targetDeckId: this.targetDeckId,
|
targetDeckId: this.targetDeckId,
|
||||||
removedConfigIds: this.removedConfigs,
|
removedConfigIds: this.removedConfigs,
|
||||||
configs,
|
configs,
|
||||||
applyToChildren,
|
mode,
|
||||||
cardStateCustomizer: get(this.cardStateCustomizer),
|
cardStateCustomizer: get(this.cardStateCustomizer),
|
||||||
limits: get(this.deckLimits),
|
limits: get(this.deckLimits),
|
||||||
newCardsIgnoreReviewLimit: get(this.newCardsIgnoreReviewLimit),
|
newCardsIgnoreReviewLimit: get(this.newCardsIgnoreReviewLimit),
|
||||||
|
@ -210,9 +211,9 @@ export class DeckOptionsState {
|
||||||
return this._presetAssignmentsChanged;
|
return this._presetAssignmentsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(applyToChildren: boolean): Promise<void> {
|
async save(mode: UpdateDeckConfigsMode): Promise<void> {
|
||||||
await updateDeckConfigs(
|
await updateDeckConfigs(
|
||||||
this.dataForSaving(applyToChildren),
|
this.dataForSaving(mode),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue