Splitting to deck and srs
This commit is contained in:
1 parent
c74a0eb311
commit
22b455a640
11 files changed
+461
-441
No files matched your search
+3
-1
@@ -3,14 +3,16 @@ use hashbrown::HashMap;
|
|||||||
use iced::{keyboard, window, Subscription};
|
use iced::{keyboard, window, Subscription};
|
||||||
use iced_core::{Font, Size, Theme};
|
use iced_core::{Font, Size, Theme};
|
||||||
use iced_core::window::Position;
|
use iced_core::window::Position;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
use iced_core::window::settings::PlatformSpecific;
|
use iced_core::window::settings::PlatformSpecific;
|
||||||
use log::info;
|
use log::info;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
use crate::core::deck::DeckSettings;
|
||||||
use crate::data_provider::card_sets::load_sets;
|
use crate::data_provider::card_sets::load_sets;
|
||||||
use crate::data_provider::settings::get_setting;
|
use crate::data_provider::settings::get_setting;
|
||||||
use crate::data_provider::sqlite::default_connection;
|
use crate::data_provider::sqlite::default_connection;
|
||||||
use crate::data_provider::words::{load_word_groups, load_words};
|
use crate::data_provider::words::{load_word_groups, load_words};
|
||||||
use crate::lang::{DeckSettings, Id, WordData, WordGroup};
|
use crate::lang::{Id, WordData, WordGroup};
|
||||||
use crate::navigation::{AppSettings, RootMessage, ScreenState};
|
use crate::navigation::{AppSettings, RootMessage, ScreenState};
|
||||||
use crate::navigation::RootMessage::Keyboard;
|
use crate::navigation::RootMessage::Keyboard;
|
||||||
pub const USER_FONT: Font = Font::with_name("Noto Sans JP");
|
pub const USER_FONT: Font = Font::with_name("Noto Sans JP");
|
||||||
|
|||||||
+3
-1
@@ -1 +1,3 @@
|
|||||||
pub mod kana_set;
|
pub mod kana_set;
|
||||||
|
pub mod srs;
|
||||||
|
pub mod deck;
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
use rayon::iter::ParallelIterator;
|
||||||
|
use rayon::iter::IndexedParallelIterator;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use log::info;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use rayon::prelude::IntoParallelRefIterator;
|
||||||
|
use rhai::{Engine, Scope};
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::core::srs::{OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, WordOpenMode, WorstWordsSRSModule, FADE_PER_DAY, MAX_SCORE};
|
||||||
|
use crate::data_provider::card_stats::{load_stats_of_deck, update_stat_score};
|
||||||
|
use crate::data_provider::history::{push_note, HistoryItem};
|
||||||
|
use crate::lang::{ Id, WordData};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DeckSettings {
|
||||||
|
pub id: Id,
|
||||||
|
pub name: String,
|
||||||
|
pub forward: String,
|
||||||
|
pub backward: String,
|
||||||
|
pub filter: String,
|
||||||
|
pub count: Option<usize>,
|
||||||
|
pub worst_words_list: Option<Vec<WordData>>,
|
||||||
|
pub open_mode: OrderMode,
|
||||||
|
}
|
||||||
|
impl DeckSettings {
|
||||||
|
pub(crate) fn with_name(name: String) -> DeckSettings {
|
||||||
|
DeckSettings {
|
||||||
|
id: 0.into(),
|
||||||
|
name,
|
||||||
|
forward: "".to_string(),
|
||||||
|
backward: "".to_string(),
|
||||||
|
filter: "true".to_string(),
|
||||||
|
count: None,
|
||||||
|
worst_words_list: None,
|
||||||
|
open_mode: OrderMode::Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn check_filter(&self) -> bool {
|
||||||
|
let time = Instant::now();
|
||||||
|
let engine = Engine::new();
|
||||||
|
let ast = engine.compile(&self.filter);
|
||||||
|
info!("Compile time is {:?}", time.elapsed());
|
||||||
|
ast.is_ok()
|
||||||
|
}
|
||||||
|
pub fn get_word_list(&self, state: &AppState) -> Vec<usize> {
|
||||||
|
let time = Instant::now();
|
||||||
|
let mut list = vec![];
|
||||||
|
let engine = Engine::new();
|
||||||
|
|
||||||
|
let ast = engine.compile(&self.filter);
|
||||||
|
if let Ok(ast) = ast {
|
||||||
|
let groups = &state.word_groups;
|
||||||
|
|
||||||
|
list = state
|
||||||
|
.dictionary
|
||||||
|
.par_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, word)| {
|
||||||
|
let mut more = rhai::Map::new();
|
||||||
|
for iced in &word.additional {
|
||||||
|
more.insert(iced.0.clone().into(), iced.1.clone().into());
|
||||||
|
}
|
||||||
|
let mut scope = Scope::new();
|
||||||
|
scope
|
||||||
|
.push_constant("id", word.id.0 as i64)
|
||||||
|
.push_constant("key", word.key.clone())
|
||||||
|
.push_constant("value", word.value.clone())
|
||||||
|
.push_constant("tags", word.tags.clone())
|
||||||
|
.push_constant("more", more)
|
||||||
|
.push_constant(
|
||||||
|
"group",
|
||||||
|
groups
|
||||||
|
.iter()
|
||||||
|
.find(|g| g.id == word.group_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap()
|
||||||
|
.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast);
|
||||||
|
result.is_ok() && result.unwrap()
|
||||||
|
})
|
||||||
|
.map(|(index, _)| index)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("Collecting available words is {:?}", time.elapsed());
|
||||||
|
|
||||||
|
list
|
||||||
|
} else {
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn require_speech(&self) -> bool {
|
||||||
|
self.forward == "speech" || self.backward == "speech"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DeckData {
|
||||||
|
pub(crate) words: Vec<WordData>,
|
||||||
|
pub(crate) set: Vec<CardStatistics>,
|
||||||
|
current_word_index: Option<usize>,
|
||||||
|
state: Arc<Mutex<AppState>>,
|
||||||
|
order_module: OrderModule,
|
||||||
|
settings: DeckSettings,
|
||||||
|
}
|
||||||
|
impl DeckData {
|
||||||
|
pub fn new(settings: &DeckSettings, state: Arc<Mutex<AppState>>) -> Self {
|
||||||
|
let state_for = state.clone();
|
||||||
|
let state_locked = state.lock();
|
||||||
|
|
||||||
|
let stats = load_stats_of_deck(settings, &state_locked.connection);
|
||||||
|
let words = stats
|
||||||
|
.iter()
|
||||||
|
.map(|stat| {
|
||||||
|
state_locked
|
||||||
|
.dictionary
|
||||||
|
.binary_search_by_key(&stat.word_id, |w| w.id)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.map(|index| &state_locked.dictionary[index])
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
set: stats,
|
||||||
|
words,
|
||||||
|
current_word_index: None,
|
||||||
|
state: state_for,
|
||||||
|
order_module: match settings.open_mode {
|
||||||
|
OrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()),
|
||||||
|
OrderMode::TrainWorstFirst => {
|
||||||
|
OrderModule::WorstWordsSRS(WorstWordsSRSModule::new())
|
||||||
|
}
|
||||||
|
OrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()),
|
||||||
|
},
|
||||||
|
settings: settings.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) -> (WordData, CardStatistics) {
|
||||||
|
let index = match self.order_module.clone() {
|
||||||
|
OrderModule::SemiRandomSRS(mut module) => {
|
||||||
|
if !module.initialized {
|
||||||
|
module.init(self)
|
||||||
|
}
|
||||||
|
let index = module.next(self);
|
||||||
|
self.order_module = OrderModule::SemiRandomSRS(module);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
OrderModule::RandomSRS(mut module) => {
|
||||||
|
if !module.initialized {
|
||||||
|
module.init(self)
|
||||||
|
}
|
||||||
|
let index = module.next(self);
|
||||||
|
self.order_module = OrderModule::RandomSRS(module);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
OrderModule::WorstWordsSRS(mut module) => {
|
||||||
|
if !module.initialized {
|
||||||
|
module.init(self)
|
||||||
|
}
|
||||||
|
let index = module.next(self);
|
||||||
|
self.order_module = OrderModule::WorstWordsSRS(module);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.current_word_index = Some(index);
|
||||||
|
(self.words[index].clone(), self.set[index].clone())
|
||||||
|
}
|
||||||
|
pub fn open(&mut self, status: WordOpenMode) {
|
||||||
|
if self.current_word_index.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let index = self.current_word_index.unwrap();
|
||||||
|
|
||||||
|
let word : &mut CardStatistics = &mut self.set[index];
|
||||||
|
let old_score = word.score;
|
||||||
|
word.update(status);
|
||||||
|
let new_score = word.score;
|
||||||
|
|
||||||
|
match self.order_module {
|
||||||
|
OrderModule::SemiRandomSRS(ref mut module) => {
|
||||||
|
module.open(status, index, word);
|
||||||
|
}
|
||||||
|
OrderModule::RandomSRS(ref mut module) => {
|
||||||
|
module.open(status, index, word);
|
||||||
|
}
|
||||||
|
OrderModule::WorstWordsSRS(ref mut module) => {
|
||||||
|
module.open(status, index, word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_stat_score(word, &self.state.lock().connection);
|
||||||
|
let _ = push_note(
|
||||||
|
self.settings.id,
|
||||||
|
HistoryItem {
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
word_id: word.word_id,
|
||||||
|
mode: status,
|
||||||
|
before: old_score,
|
||||||
|
after: new_score,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.set.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct CardStatistics {
|
||||||
|
pub id: Id,
|
||||||
|
pub word_id: Id,
|
||||||
|
pub set_id: Id,
|
||||||
|
pub last_open: DateTime<Utc>,
|
||||||
|
pub score: u8,
|
||||||
|
}
|
||||||
|
impl CardStatistics {
|
||||||
|
pub fn update(&mut self, status: WordOpenMode) {
|
||||||
|
match status {
|
||||||
|
WordOpenMode::Easy => {
|
||||||
|
self.score = (self.calculated_score() + 5.0).round() as u8;
|
||||||
|
}
|
||||||
|
WordOpenMode::Ok => self.score = (self.calculated_score() + 2.0).round() as u8,
|
||||||
|
WordOpenMode::Hard => {
|
||||||
|
self.score = (self.calculated_score() * 0.75).round() as u8;
|
||||||
|
}
|
||||||
|
WordOpenMode::None => {
|
||||||
|
self.score = (self.calculated_score() * 0.4) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.score = self.score.clamp(1, MAX_SCORE);
|
||||||
|
self.last_open = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculated_score(&self) -> f32 {
|
||||||
|
let time = Utc::now() - self.last_open;
|
||||||
|
let days = time.num_days();
|
||||||
|
let multiplier = FADE_PER_DAY.powi(days as i32);
|
||||||
|
(self.score as f32 * multiplier).max(1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
use crate::core::deck::{CardStatistics, DeckData};
|
||||||
|
use rand::distr::weighted::WeightedIndex;
|
||||||
|
use rand::prelude::ThreadRng;
|
||||||
|
use rand::prelude::{Distribution, SliceRandom};
|
||||||
|
use rand::rng;
|
||||||
|
use rayon::iter::IndexedParallelIterator;
|
||||||
|
use rayon::iter::ParallelIterator;
|
||||||
|
|
||||||
|
const MAX_HISTORY_LEN: usize = 20;
|
||||||
|
const MAX_HISTORY_LEN_PART: f32 = 0.33;
|
||||||
|
pub const MAX_SCORE: u8 = 25;
|
||||||
|
pub const FADE_PER_DAY: f32 = 0.95;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Copy, Eq)]
|
||||||
|
pub enum OrderMode {
|
||||||
|
Default,
|
||||||
|
TrainWorstFirst,
|
||||||
|
FullRandom,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Default)]
|
||||||
|
pub enum AppendMode {
|
||||||
|
Full,
|
||||||
|
#[default]
|
||||||
|
Manual,
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum OrderModule {
|
||||||
|
SemiRandomSRS(SemiRandomSRSModule),
|
||||||
|
RandomSRS(RandomSRSModule),
|
||||||
|
WorstWordsSRS(WorstWordsSRSModule),
|
||||||
|
}
|
||||||
|
pub trait SRSModule {
|
||||||
|
fn next(&mut self, set: &mut DeckData) -> usize;
|
||||||
|
fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &CardStatistics);
|
||||||
|
fn init(&mut self, set: &mut DeckData);
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RandomSRSModule {
|
||||||
|
basket: Vec<usize>,
|
||||||
|
pub(crate) initialized: bool,
|
||||||
|
}
|
||||||
|
impl RandomSRSModule {
|
||||||
|
pub(crate) fn new() -> RandomSRSModule {
|
||||||
|
Self {
|
||||||
|
basket: vec![],
|
||||||
|
initialized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl SRSModule for RandomSRSModule {
|
||||||
|
fn next(&mut self, set: &mut DeckData) -> usize {
|
||||||
|
match self.basket.pop() {
|
||||||
|
Some(index) => index,
|
||||||
|
None => {
|
||||||
|
self.basket = (0..set.words.len()).collect::<Vec<usize>>();
|
||||||
|
self.basket.shuffle(&mut rand::rng());
|
||||||
|
self.basket.pop().expect("empty basket")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {}
|
||||||
|
|
||||||
|
fn init(&mut self, set: &mut DeckData) {
|
||||||
|
self.initialized = true;
|
||||||
|
self.basket = (0..set.set.len()).collect::<Vec<usize>>();
|
||||||
|
self.basket.shuffle(&mut rand::rng())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SemiRandomSRSModule {
|
||||||
|
history: Vec<usize>,
|
||||||
|
last_weights: WeightedIndex<f32>,
|
||||||
|
generator: ThreadRng,
|
||||||
|
pub(crate) initialized: bool,
|
||||||
|
}
|
||||||
|
impl SemiRandomSRSModule {
|
||||||
|
pub(crate) fn new() -> SemiRandomSRSModule {
|
||||||
|
SemiRandomSRSModule {
|
||||||
|
history: vec![],
|
||||||
|
last_weights: WeightedIndex::new([1.0]).unwrap(),
|
||||||
|
generator: rng(),
|
||||||
|
initialized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl SRSModule for SemiRandomSRSModule {
|
||||||
|
fn next(&mut self, set: &mut DeckData) -> usize {
|
||||||
|
let index = self.last_weights.sample(&mut self.generator);
|
||||||
|
|
||||||
|
if self.history.contains(&index) && self.history.len() != 1 {
|
||||||
|
return self.next(set);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.history.len() == self.history_len(set) {
|
||||||
|
self.history.remove(0);
|
||||||
|
}
|
||||||
|
self.history.push(index);
|
||||||
|
|
||||||
|
index
|
||||||
|
}
|
||||||
|
fn open(&mut self, _: WordOpenMode, index: usize, word: &CardStatistics) {
|
||||||
|
let new_weight = (100.0 / word.calculated_score()).powf(2.0);
|
||||||
|
self.last_weights
|
||||||
|
.update_weights(&[(index, &new_weight)])
|
||||||
|
.expect("update weights error");
|
||||||
|
}
|
||||||
|
fn init(&mut self, set: &mut DeckData) {
|
||||||
|
self.initialized = true;
|
||||||
|
let weights = set
|
||||||
|
.set
|
||||||
|
.iter()
|
||||||
|
.map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0)
|
||||||
|
.collect::<Vec<f32>>();
|
||||||
|
self.last_weights = WeightedIndex::new(weights).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl SemiRandomSRSModule {
|
||||||
|
fn history_len(&self, set: &DeckData) -> usize {
|
||||||
|
((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WorstWordsSRSModule {
|
||||||
|
pub(crate) initialized: bool,
|
||||||
|
pool_size: usize,
|
||||||
|
rounds_remaining: u8,
|
||||||
|
pool: Vec<usize>,
|
||||||
|
queue: Vec<usize>,
|
||||||
|
rounds_count: u8,
|
||||||
|
}
|
||||||
|
impl SRSModule for WorstWordsSRSModule {
|
||||||
|
fn next(&mut self, set: &mut DeckData) -> usize {
|
||||||
|
if self.rounds_remaining == 0 {
|
||||||
|
self.fill_pool(set);
|
||||||
|
self.rounds_remaining = self.rounds_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.queue.pop() {
|
||||||
|
Some(index) => index,
|
||||||
|
None => {
|
||||||
|
self.queue.append(&mut self.pool.clone());
|
||||||
|
self.rounds_remaining -= 1;
|
||||||
|
self.queue.pop().expect("empty queue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {}
|
||||||
|
|
||||||
|
fn init(&mut self, _: &mut DeckData) {
|
||||||
|
self.initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl WorstWordsSRSModule {
|
||||||
|
pub(crate) fn new() -> WorstWordsSRSModule {
|
||||||
|
WorstWordsSRSModule {
|
||||||
|
initialized: false,
|
||||||
|
pool_size: 15,
|
||||||
|
rounds_count: 2,
|
||||||
|
rounds_remaining: 0,
|
||||||
|
pool: vec![],
|
||||||
|
queue: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn fill_pool(&mut self, set: &DeckData) {
|
||||||
|
let mut sorted = set
|
||||||
|
.set
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.zip(0..set.set.len())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
sorted.sort_by_key(|c| c.0.score);
|
||||||
|
let mut worst = sorted[0..self.pool_size]
|
||||||
|
.iter()
|
||||||
|
.map(|(_, index)| *index)
|
||||||
|
.collect::<Vec<usize>>();
|
||||||
|
worst.shuffle(&mut rand::rng());
|
||||||
|
self.pool = worst;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum WordOpenMode {
|
||||||
|
Easy,
|
||||||
|
Ok,
|
||||||
|
Hard,
|
||||||
|
None,
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::lang::{DeckSettings, OrderMode};
|
use crate::core::srs::{ OrderMode};
|
||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
use crate::core::deck::DeckSettings;
|
||||||
|
|
||||||
pub fn load_sets(connection: &Connection) -> Vec<DeckSettings> {
|
pub fn load_sets(connection: &Connection) -> Vec<DeckSettings> {
|
||||||
let mut stmt = connection
|
let mut stmt = connection
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::lang::{CardStatistics, DeckSettings};
|
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use crate::core::deck::{CardStatistics, DeckSettings};
|
||||||
|
|
||||||
pub fn load_stats_of_deck(set: &DeckSettings, connection: &Connection) -> Vec<CardStatistics> {
|
pub fn load_stats_of_deck(set: &DeckSettings, connection: &Connection) -> Vec<CardStatistics> {
|
||||||
let mut stmt = connection
|
let mut stmt = connection
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use crate::fs::*;
|
use crate::fs::*;
|
||||||
use crate::lang::{Id, WordOpenMode};
|
use crate::lang::{Id};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use crate::core::srs::WordOpenMode;
|
||||||
use crate::notification::NotificationData;
|
use crate::notification::NotificationData;
|
||||||
use crate::notification::NotificationData::PermissionDenied;
|
use crate::notification::NotificationData::PermissionDenied;
|
||||||
|
|
||||||
|
|||||||
+8
-433
@@ -1,18 +1,3 @@
|
|||||||
use crate::data_provider::card_stats::load_stats_of_deck;
|
|
||||||
use crate::data_provider::card_stats::update_stat_score;
|
|
||||||
use crate::data_provider::history::{push_note, HistoryItem};
|
|
||||||
use crate::app::AppState;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use rand::distr::weighted::WeightedIndex;
|
|
||||||
use rand::distr::Distribution;
|
|
||||||
use rand::prelude::SliceRandom;
|
|
||||||
use rand::rng;
|
|
||||||
use rand::rngs::ThreadRng;
|
|
||||||
use rayon::iter::IndexedParallelIterator;
|
|
||||||
use rayon::iter::IntoParallelRefIterator;
|
|
||||||
use rayon::iter::ParallelIterator;
|
|
||||||
use rhai::{Engine, Scope};
|
|
||||||
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
|
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
|
||||||
use rusqlite::ToSql;
|
use rusqlite::ToSql;
|
||||||
use std::cmp::PartialEq;
|
use std::cmp::PartialEq;
|
||||||
@@ -20,16 +5,9 @@ use std::collections::HashMap;
|
|||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Instant;
|
|
||||||
use log::info;
|
|
||||||
|
|
||||||
const MAX_HISTORY_LEN: usize = 20;
|
|
||||||
const MAX_HISTORY_LEN_PART: f32 = 0.33;
|
|
||||||
const MAX_SCORE: u8 = 25;
|
|
||||||
const FADE_PER_DAY: f32 = 0.95;
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Hash, Copy, Ord, PartialOrd)]
|
#[derive(Clone, PartialEq, Eq, Debug, Hash, Copy, Ord, PartialOrd)]
|
||||||
pub struct Id(u32);
|
pub struct Id(pub(crate) u32);
|
||||||
pub const INVALID_ID: Id = Id(0);
|
pub const INVALID_ID: Id = Id(0);
|
||||||
impl FromStr for Id {
|
impl FromStr for Id {
|
||||||
type Err = ParseIntError;
|
type Err = ParseIntError;
|
||||||
@@ -83,7 +61,6 @@ impl Id {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct WordData {
|
pub struct WordData {
|
||||||
pub id: Id,
|
pub id: Id,
|
||||||
@@ -93,6 +70,13 @@ pub struct WordData {
|
|||||||
pub additional: HashMap<String, String>,
|
pub additional: HashMap<String, String>,
|
||||||
pub group_id: Id,
|
pub group_id: Id,
|
||||||
}
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WordGroup {
|
||||||
|
pub id: Id,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
impl WordData {
|
impl WordData {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -105,412 +89,3 @@ impl WordData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct WordGroup {
|
|
||||||
pub id: Id,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub struct CardStatistics {
|
|
||||||
pub id: Id,
|
|
||||||
pub word_id: Id,
|
|
||||||
pub set_id: Id,
|
|
||||||
pub last_open: DateTime<Utc>,
|
|
||||||
pub score: u8,
|
|
||||||
}
|
|
||||||
impl CardStatistics {
|
|
||||||
pub fn update(&mut self, status: WordOpenMode) {
|
|
||||||
match status {
|
|
||||||
WordOpenMode::Easy => {
|
|
||||||
self.score = (self.calculated_score() + 5.0).round() as u8;
|
|
||||||
}
|
|
||||||
WordOpenMode::Ok => self.score = (self.calculated_score() + 2.0).round() as u8,
|
|
||||||
WordOpenMode::Hard => {
|
|
||||||
self.score = (self.calculated_score() * 0.75).round() as u8;
|
|
||||||
}
|
|
||||||
WordOpenMode::None => {
|
|
||||||
self.score = (self.calculated_score() * 0.4) as u8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.score = self.score.clamp(1, MAX_SCORE);
|
|
||||||
self.last_open = Utc::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn calculated_score(&self) -> f32 {
|
|
||||||
let time = Utc::now() - self.last_open;
|
|
||||||
let days = time.num_days();
|
|
||||||
let multiplier = FADE_PER_DAY.powi(days as i32);
|
|
||||||
(self.score as f32 * multiplier).max(1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub enum WordOpenMode {
|
|
||||||
Easy,
|
|
||||||
Ok,
|
|
||||||
Hard,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct DeckData {
|
|
||||||
words: Vec<WordData>,
|
|
||||||
set: Vec<CardStatistics>,
|
|
||||||
current_word_index: Option<usize>,
|
|
||||||
state: Arc<Mutex<AppState>>,
|
|
||||||
order_module: OrderModule,
|
|
||||||
settings: DeckSettings,
|
|
||||||
}
|
|
||||||
impl DeckData {
|
|
||||||
pub fn new(settings: &DeckSettings, state: Arc<Mutex<AppState>>) -> Self {
|
|
||||||
let state_for = state.clone();
|
|
||||||
let state_locked = state.lock();
|
|
||||||
|
|
||||||
let stats = load_stats_of_deck(settings, &state_locked.connection);
|
|
||||||
let words = stats
|
|
||||||
.iter()
|
|
||||||
.map(|stat| {
|
|
||||||
state_locked
|
|
||||||
.dictionary
|
|
||||||
.binary_search_by_key(&stat.word_id, |w| w.id)
|
|
||||||
.unwrap()
|
|
||||||
})
|
|
||||||
.map(|index| &state_locked.dictionary[index])
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
set: stats,
|
|
||||||
words,
|
|
||||||
current_word_index: None,
|
|
||||||
state: state_for,
|
|
||||||
order_module: match settings.open_mode {
|
|
||||||
OrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()),
|
|
||||||
OrderMode::TrainWorstFirst => {
|
|
||||||
OrderModule::WorstWordsSRS(WorstWordsSRSModule::new())
|
|
||||||
}
|
|
||||||
OrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()),
|
|
||||||
},
|
|
||||||
settings: settings.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next(&mut self) -> (WordData, CardStatistics) {
|
|
||||||
let index = match self.order_module.clone() {
|
|
||||||
OrderModule::SemiRandomSRS(mut module) => {
|
|
||||||
if !module.initialized {
|
|
||||||
module.init(self)
|
|
||||||
}
|
|
||||||
let index = module.next(self);
|
|
||||||
self.order_module = OrderModule::SemiRandomSRS(module);
|
|
||||||
index
|
|
||||||
}
|
|
||||||
OrderModule::RandomSRS(mut module) => {
|
|
||||||
if !module.initialized {
|
|
||||||
module.init(self)
|
|
||||||
}
|
|
||||||
let index = module.next(self);
|
|
||||||
self.order_module = OrderModule::RandomSRS(module);
|
|
||||||
index
|
|
||||||
}
|
|
||||||
OrderModule::WorstWordsSRS(mut module) => {
|
|
||||||
if !module.initialized {
|
|
||||||
module.init(self)
|
|
||||||
}
|
|
||||||
let index = module.next(self);
|
|
||||||
self.order_module = OrderModule::WorstWordsSRS(module);
|
|
||||||
index
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.current_word_index = Some(index);
|
|
||||||
(self.words[index].clone(), self.set[index].clone())
|
|
||||||
}
|
|
||||||
pub fn open(&mut self, status: WordOpenMode) {
|
|
||||||
if self.current_word_index.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let index = self.current_word_index.unwrap();
|
|
||||||
|
|
||||||
let word : &mut CardStatistics = &mut self.set[index];
|
|
||||||
let old_score = word.score;
|
|
||||||
word.update(status);
|
|
||||||
let new_score = word.score;
|
|
||||||
|
|
||||||
match self.order_module {
|
|
||||||
OrderModule::SemiRandomSRS(ref mut module) => {
|
|
||||||
module.open(status, index, word);
|
|
||||||
}
|
|
||||||
OrderModule::RandomSRS(ref mut module) => {
|
|
||||||
module.open(status, index, word);
|
|
||||||
}
|
|
||||||
OrderModule::WorstWordsSRS(ref mut module) => {
|
|
||||||
module.open(status, index, word);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
update_stat_score(word, &self.state.lock().connection);
|
|
||||||
let _ = push_note(
|
|
||||||
self.settings.id,
|
|
||||||
HistoryItem {
|
|
||||||
timestamp: Utc::now(),
|
|
||||||
word_id: word.word_id,
|
|
||||||
mode: status,
|
|
||||||
before: old_score,
|
|
||||||
after: new_score,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.set.len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, Copy, Eq)]
|
|
||||||
pub enum OrderMode {
|
|
||||||
Default,
|
|
||||||
TrainWorstFirst,
|
|
||||||
FullRandom,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Default)]
|
|
||||||
pub enum AppendMode {
|
|
||||||
Full,
|
|
||||||
#[default]
|
|
||||||
Manual,
|
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
|
||||||
enum OrderModule {
|
|
||||||
SemiRandomSRS(SemiRandomSRSModule),
|
|
||||||
RandomSRS(RandomSRSModule),
|
|
||||||
WorstWordsSRS(WorstWordsSRSModule),
|
|
||||||
}
|
|
||||||
trait SRSModule {
|
|
||||||
fn next(&mut self, set: &mut DeckData) -> usize;
|
|
||||||
fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &CardStatistics);
|
|
||||||
fn init(&mut self, set: &mut DeckData);
|
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct RandomSRSModule {
|
|
||||||
basket: Vec<usize>,
|
|
||||||
initialized: bool,
|
|
||||||
}
|
|
||||||
impl RandomSRSModule {
|
|
||||||
fn new() -> RandomSRSModule {
|
|
||||||
Self {
|
|
||||||
basket: vec![],
|
|
||||||
initialized: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SRSModule for RandomSRSModule {
|
|
||||||
fn next(&mut self, set: &mut DeckData) -> usize {
|
|
||||||
match self.basket.pop() {
|
|
||||||
Some(index) => index,
|
|
||||||
None => {
|
|
||||||
self.basket = (0..set.words.len()).collect::<Vec<usize>>();
|
|
||||||
self.basket.shuffle(&mut rand::rng());
|
|
||||||
self.basket.pop().expect("empty basket")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {}
|
|
||||||
|
|
||||||
fn init(&mut self, set: &mut DeckData) {
|
|
||||||
self.initialized = true;
|
|
||||||
self.basket = (0..set.set.len()).collect::<Vec<usize>>();
|
|
||||||
self.basket.shuffle(&mut rand::rng())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct SemiRandomSRSModule {
|
|
||||||
history: Vec<usize>,
|
|
||||||
last_weights: WeightedIndex<f32>,
|
|
||||||
generator: ThreadRng,
|
|
||||||
initialized: bool,
|
|
||||||
}
|
|
||||||
impl SemiRandomSRSModule {
|
|
||||||
fn new() -> SemiRandomSRSModule {
|
|
||||||
SemiRandomSRSModule {
|
|
||||||
history: vec![],
|
|
||||||
last_weights: WeightedIndex::new([1.0]).unwrap(),
|
|
||||||
generator: rng(),
|
|
||||||
initialized: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SRSModule for SemiRandomSRSModule {
|
|
||||||
fn next(&mut self, set: &mut DeckData) -> usize {
|
|
||||||
let index = self.last_weights.sample(&mut self.generator);
|
|
||||||
|
|
||||||
if self.history.contains(&index) && self.history.len() != 1 {
|
|
||||||
return self.next(set);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.history.len() == self.history_len(set) {
|
|
||||||
self.history.remove(0);
|
|
||||||
}
|
|
||||||
self.history.push(index);
|
|
||||||
|
|
||||||
index
|
|
||||||
}
|
|
||||||
fn open(&mut self, _: WordOpenMode, index: usize, word: &CardStatistics) {
|
|
||||||
let new_weight = (100.0 / word.calculated_score()).powf(2.0);
|
|
||||||
self.last_weights
|
|
||||||
.update_weights(&[(index, &new_weight)])
|
|
||||||
.expect("update weights error");
|
|
||||||
}
|
|
||||||
fn init(&mut self, set: &mut DeckData) {
|
|
||||||
self.initialized = true;
|
|
||||||
let weights = set
|
|
||||||
.set
|
|
||||||
.iter()
|
|
||||||
.map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0)
|
|
||||||
.collect::<Vec<f32>>();
|
|
||||||
self.last_weights = WeightedIndex::new(weights).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SemiRandomSRSModule {
|
|
||||||
fn history_len(&self, set: &DeckData) -> usize {
|
|
||||||
((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct WorstWordsSRSModule {
|
|
||||||
initialized: bool,
|
|
||||||
pool_size: usize,
|
|
||||||
rounds_remaining: u8,
|
|
||||||
pool: Vec<usize>,
|
|
||||||
queue: Vec<usize>,
|
|
||||||
rounds_count: u8,
|
|
||||||
}
|
|
||||||
impl SRSModule for WorstWordsSRSModule {
|
|
||||||
fn next(&mut self, set: &mut DeckData) -> usize {
|
|
||||||
if self.rounds_remaining == 0 {
|
|
||||||
self.fill_pool(set);
|
|
||||||
self.rounds_remaining = self.rounds_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.queue.pop() {
|
|
||||||
Some(index) => index,
|
|
||||||
None => {
|
|
||||||
self.queue.append(&mut self.pool.clone());
|
|
||||||
self.rounds_remaining -= 1;
|
|
||||||
self.queue.pop().expect("empty queue")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {}
|
|
||||||
|
|
||||||
fn init(&mut self, _: &mut DeckData) {
|
|
||||||
self.initialized = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl WorstWordsSRSModule {
|
|
||||||
fn new() -> WorstWordsSRSModule {
|
|
||||||
WorstWordsSRSModule {
|
|
||||||
initialized: false,
|
|
||||||
pool_size: 15,
|
|
||||||
rounds_count: 2,
|
|
||||||
rounds_remaining: 0,
|
|
||||||
pool: vec![],
|
|
||||||
queue: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn fill_pool(&mut self, set: &DeckData) {
|
|
||||||
let mut sorted = set
|
|
||||||
.set
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.zip(0..set.set.len())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
sorted.sort_by_key(|c| c.0.score);
|
|
||||||
let mut worst = sorted[0..self.pool_size]
|
|
||||||
.iter()
|
|
||||||
.map(|(_, index)| *index)
|
|
||||||
.collect::<Vec<usize>>();
|
|
||||||
worst.shuffle(&mut rand::rng());
|
|
||||||
self.pool = worst;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct DeckSettings {
|
|
||||||
pub id: Id,
|
|
||||||
pub name: String,
|
|
||||||
pub forward: String,
|
|
||||||
pub backward: String,
|
|
||||||
pub filter: String,
|
|
||||||
pub count: Option<usize>,
|
|
||||||
pub worst_words_list: Option<Vec<WordData>>,
|
|
||||||
pub open_mode: OrderMode,
|
|
||||||
}
|
|
||||||
impl DeckSettings {
|
|
||||||
pub(crate) fn with_name(name: String) -> DeckSettings {
|
|
||||||
DeckSettings {
|
|
||||||
id: 0.into(),
|
|
||||||
name,
|
|
||||||
forward: "".to_string(),
|
|
||||||
backward: "".to_string(),
|
|
||||||
filter: "true".to_string(),
|
|
||||||
count: None,
|
|
||||||
worst_words_list: None,
|
|
||||||
open_mode: OrderMode::Default,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn check_filter(&self) -> bool {
|
|
||||||
let time = Instant::now();
|
|
||||||
let engine = Engine::new();
|
|
||||||
let ast = engine.compile(&self.filter);
|
|
||||||
info!("Compile time is {:?}", time.elapsed());
|
|
||||||
ast.is_ok()
|
|
||||||
}
|
|
||||||
pub fn get_word_list(&self, state: &AppState) -> Vec<usize> {
|
|
||||||
let time = Instant::now();
|
|
||||||
let mut list = vec![];
|
|
||||||
let engine = Engine::new();
|
|
||||||
|
|
||||||
let ast = engine.compile(&self.filter);
|
|
||||||
if let Ok(ast) = ast {
|
|
||||||
let groups = &state.word_groups;
|
|
||||||
|
|
||||||
list = state
|
|
||||||
.dictionary
|
|
||||||
.par_iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter(|(_, word)| {
|
|
||||||
let mut more = rhai::Map::new();
|
|
||||||
for iced in &word.additional {
|
|
||||||
more.insert(iced.0.clone().into(), iced.1.clone().into());
|
|
||||||
}
|
|
||||||
let mut scope = Scope::new();
|
|
||||||
scope
|
|
||||||
.push_constant("id", word.id.0 as i64)
|
|
||||||
.push_constant("key", word.key.clone())
|
|
||||||
.push_constant("value", word.value.clone())
|
|
||||||
.push_constant("tags", word.tags.clone())
|
|
||||||
.push_constant("more", more)
|
|
||||||
.push_constant(
|
|
||||||
"group",
|
|
||||||
groups
|
|
||||||
.iter()
|
|
||||||
.find(|g| g.id == word.group_id)
|
|
||||||
.cloned()
|
|
||||||
.unwrap()
|
|
||||||
.name,
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = engine.eval_ast_with_scope::<bool>(&mut scope, &ast);
|
|
||||||
result.is_ok() && result.unwrap()
|
|
||||||
})
|
|
||||||
.map(|(index, _)| index)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
info!("Collecting available words is {:?}", time.elapsed());
|
|
||||||
|
|
||||||
list
|
|
||||||
} else {
|
|
||||||
list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn require_speech(&self) -> bool {
|
|
||||||
self.forward == "speech" || self.backward == "speech"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
use crate::data_provider::voice::get_voice;
|
use crate::data_provider::voice::get_voice;
|
||||||
use crate::lang::Id;
|
use crate::lang::Id;
|
||||||
use crate::lang::{CardStatistics, DeckData, DeckSettings, WordData, WordOpenMode};
|
use crate::lang::{ WordData};
|
||||||
use crate::navigation::Page::PreviousPage;
|
use crate::navigation::Page::PreviousPage;
|
||||||
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
|
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
|
||||||
use crate::notification::{NotificationData, NotificationRequest};
|
use crate::notification::{NotificationData, NotificationRequest};
|
||||||
@@ -17,6 +17,8 @@ use rodio::MixerDeviceSink;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
|
use crate::core::deck::{CardStatistics, DeckData, DeckSettings};
|
||||||
|
use crate::core::srs::{ WordOpenMode};
|
||||||
|
|
||||||
pub struct RepetitionState {
|
pub struct RepetitionState {
|
||||||
state: Arc<Mutex<AppState>>,
|
state: Arc<Mutex<AppState>>,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
use crate::data_provider::card_sets::{delete_set, update_deck};
|
use crate::data_provider::card_sets::{delete_set, update_deck};
|
||||||
use crate::data_provider::card_stats::{delete_stats, load_stats_of_deck};
|
use crate::data_provider::card_stats::{delete_stats, load_stats_of_deck};
|
||||||
use crate::lang::DeckSettings;
|
|
||||||
use crate::navigation::Page::PreviousPage;
|
use crate::navigation::Page::PreviousPage;
|
||||||
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
use crate::pages::repetition_settings::RepetitionSettingsMessage::*;
|
use crate::pages::repetition_settings::RepetitionSettingsMessage::*;
|
||||||
@@ -16,6 +15,7 @@ use parking_lot::Mutex;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
|
use crate::core::deck::DeckSettings;
|
||||||
|
|
||||||
pub struct RepetitionSettingsState {
|
pub struct RepetitionSettingsState {
|
||||||
set: DeckSettings,
|
set: DeckSettings,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::data_provider::card_sets::update_deck;
|
use crate::data_provider::card_sets::update_deck;
|
||||||
use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck};
|
use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck};
|
||||||
use crate::pages::history::HistoryState;
|
use crate::pages::history::HistoryState;
|
||||||
use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode};
|
use crate::lang::{Id};
|
||||||
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
|
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
|
||||||
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
use crate::pages::repetition::RepetitionState;
|
use crate::pages::repetition::RepetitionState;
|
||||||
@@ -25,6 +25,8 @@ use parking_lot::Mutex;
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
|
use crate::core::deck::{CardStatistics, DeckSettings};
|
||||||
|
use crate::core::srs::{AppendMode, OrderMode};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RepetitionsState {
|
pub struct RepetitionsState {
|
||||||
|
|||||||
Reference in new issue
Block a user