2026-08-18 12:14:35 +03:00
|
|
|
|
use crate::AppState;
|
2026-08-18 15:16:51 +03:00
|
|
|
|
use crate::data_provider::card_stats::{delete_stat, load_stats_of_deck, update_stat_score};
|
2026-08-18 12:58:02 +03:00
|
|
|
|
use crate::data_provider::history::{HistoryItem, push_note};
|
2026-04-13 22:25:58 +03:00
|
|
|
|
use chrono::{DateTime, Utc};
|
2026-08-18 12:14:35 +03:00
|
|
|
|
use rand::distr::Distribution;
|
2026-08-18 12:58:02 +03:00
|
|
|
|
use rand::distr::weighted::WeightedIndex;
|
2026-05-25 12:23:32 +03:00
|
|
|
|
use rand::prelude::SliceRandom;
|
2026-04-14 15:19:46 +03:00
|
|
|
|
use rand::rng;
|
|
|
|
|
|
use rand::rngs::ThreadRng;
|
2026-08-15 21:32:24 +03:00
|
|
|
|
use rayon::iter::IndexedParallelIterator;
|
2026-08-15 20:16:48 +03:00
|
|
|
|
use rayon::iter::IntoParallelRefIterator;
|
|
|
|
|
|
use rayon::iter::ParallelIterator;
|
|
|
|
|
|
use rhai::{Engine, Scope};
|
2026-08-18 12:47:06 +03:00
|
|
|
|
use rusqlite::ToSql;
|
2026-08-18 12:58:02 +03:00
|
|
|
|
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
|
2026-08-18 12:47:06 +03:00
|
|
|
|
use std::cmp::PartialEq;
|
2026-04-13 22:25:58 +03:00
|
|
|
|
use std::collections::HashMap;
|
2026-08-18 12:47:06 +03:00
|
|
|
|
use std::fmt::{Display, Formatter};
|
2026-08-18 12:58:02 +03:00
|
|
|
|
use std::num::ParseIntError;
|
|
|
|
|
|
use std::str::FromStr;
|
2026-04-13 22:25:58 +03:00
|
|
|
|
use std::sync::{Arc, Mutex};
|
2026-07-29 13:30:49 +03:00
|
|
|
|
use std::time::Instant;
|
2026-04-13 22:25:58 +03:00
|
|
|
|
|
2026-04-20 10:13:39 +03:00
|
|
|
|
const MAX_HISTORY_LEN: usize = 20;
|
2026-04-17 13:06:37 +03:00
|
|
|
|
const MAX_HISTORY_LEN_PART: f32 = 0.33;
|
2026-07-29 12:57:46 +03:00
|
|
|
|
const MAX_SCORE: u8 = 25;
|
2026-04-13 22:25:58 +03:00
|
|
|
|
const FADE_PER_DAY: f32 = 0.95;
|
2026-08-18 12:47:06 +03:00
|
|
|
|
#[derive(Clone, PartialEq, Eq, Debug, Hash, Copy)]
|
2026-08-18 17:45:50 +03:00
|
|
|
|
pub struct Id(u32);
|
2026-08-18 12:47:06 +03:00
|
|
|
|
pub const INVALID_ID: Id = Id(0);
|
2026-08-18 12:58:02 +03:00
|
|
|
|
impl FromStr for Id {
|
|
|
|
|
|
type Err = ParseIntError;
|
|
|
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
|
Ok(Id(s.parse::<u32>()?))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-18 17:47:37 +03:00
|
|
|
|
impl From<u32> for Id {
|
|
|
|
|
|
fn from(value: u32) -> Self {
|
|
|
|
|
|
Id(value)
|
2026-08-18 12:58:02 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Into<u32> for Id {
|
|
|
|
|
|
fn into(self) -> u32 {
|
|
|
|
|
|
self.0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
impl Display for Id {
|
|
|
|
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
|
write!(f, "{}", self.0)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl FromSql for Id {
|
|
|
|
|
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
|
|
|
|
|
match value {
|
|
|
|
|
|
ValueRef::Integer(i) => Ok(Id(i as u32)),
|
|
|
|
|
|
_ => Err(FromSqlError::InvalidType),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl ToSql for Id {
|
|
|
|
|
|
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
|
|
|
|
|
Ok(ToSqlOutput::from(self.0 as i64))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-18 17:31:19 +03:00
|
|
|
|
impl Id {
|
|
|
|
|
|
pub(crate) fn is_valid(self) -> bool {
|
|
|
|
|
|
self.0 != 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 12:12:16 +03:00
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
|
pub struct KanaSet {
|
|
|
|
|
|
name: String,
|
|
|
|
|
|
pub(crate) chars_type: KanaType,
|
|
|
|
|
|
pub(crate) dictionary: Vec<Vec<(String, String)>>,
|
|
|
|
|
|
pub(crate) include_map: [bool; 10],
|
|
|
|
|
|
}
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
|
pub enum KanaType {
|
|
|
|
|
|
Hiragana,
|
|
|
|
|
|
Katakana,
|
|
|
|
|
|
}
|
|
|
|
|
|
impl KanaSet {
|
|
|
|
|
|
pub fn hiragana() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
name: "Хирагана".to_string(),
|
|
|
|
|
|
chars_type: KanaType::Hiragana,
|
|
|
|
|
|
dictionary: vec![
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("ぁ"), String::from("a")),
|
|
|
|
|
|
(String::from("ぃ"), String::from("i")),
|
|
|
|
|
|
(String::from("ぅ"), String::from("u")),
|
|
|
|
|
|
(String::from("ぇ"), String::from("e")),
|
|
|
|
|
|
(String::from("ぉ"), String::from("o")),
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("さ"), String::from("sa")),
|
|
|
|
|
|
(String::from("し"), String::from("shi")),
|
|
|
|
|
|
(String::from("す"), String::from("su")),
|
|
|
|
|
|
(String::from("せ"), String::from("se")),
|
|
|
|
|
|
(String::from("そ"), String::from("so")),
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("か"), String::from("ka")),
|
|
|
|
|
|
(String::from("き"), String::from("ki")),
|
|
|
|
|
|
(String::from("く"), String::from("ku")),
|
|
|
|
|
|
(String::from("け"), String::from("ke")),
|
|
|
|
|
|
(String::from("こ"), String::from("ko")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «та» (たちつてと)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("た"), String::from("ta")),
|
|
|
|
|
|
(String::from("ち"), String::from("chi")),
|
|
|
|
|
|
(String::from("つ"), String::from("tsu")),
|
|
|
|
|
|
(String::from("て"), String::from("te")),
|
|
|
|
|
|
(String::from("と"), String::from("to")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «на» (なにぬねの)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("な"), String::from("na")),
|
|
|
|
|
|
(String::from("に"), String::from("ni")),
|
|
|
|
|
|
(String::from("ぬ"), String::from("nu")),
|
|
|
|
|
|
(String::from("ね"), String::from("ne")),
|
|
|
|
|
|
(String::from("の"), String::from("no")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «ха» (はひふへほ)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("は"), String::from("ha")),
|
|
|
|
|
|
(String::from("ひ"), String::from("hi")),
|
|
|
|
|
|
(String::from("ふ"), String::from("fu")),
|
|
|
|
|
|
(String::from("へ"), String::from("he")),
|
|
|
|
|
|
(String::from("ほ"), String::from("ho")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «ма» (まみむめも)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("ま"), String::from("ma")),
|
|
|
|
|
|
(String::from("み"), String::from("mi")),
|
|
|
|
|
|
(String::from("む"), String::from("mu")),
|
|
|
|
|
|
(String::from("め"), String::from("me")),
|
|
|
|
|
|
(String::from("も"), String::from("mo")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «я» (やゆよ) — только 3 символа
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("や"), String::from("ya")),
|
|
|
|
|
|
(String::from("ゆ"), String::from("yu")),
|
|
|
|
|
|
(String::from("よ"), String::from("yo")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «ра» (らりるれろ)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("ら"), String::from("ra")),
|
|
|
|
|
|
(String::from("り"), String::from("ri")),
|
|
|
|
|
|
(String::from("る"), String::from("ru")),
|
|
|
|
|
|
(String::from("れ"), String::from("re")),
|
|
|
|
|
|
(String::from("ろ"), String::from("ro")),
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("わ"), String::from("wa")),
|
|
|
|
|
|
(String::from("を"), String::from("wo")),
|
|
|
|
|
|
(String::from("ん"), String::from("n")),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
include_map: [true; 10],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn katakana() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
name: "Катакана".to_string(),
|
|
|
|
|
|
chars_type: KanaType::Katakana,
|
|
|
|
|
|
dictionary: vec![
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("ァ"), String::from("a")),
|
|
|
|
|
|
(String::from("ィ"), String::from("i")),
|
|
|
|
|
|
(String::from("ゥ"), String::from("u")),
|
|
|
|
|
|
(String::from("ェ"), String::from("e")),
|
|
|
|
|
|
(String::from("ォ"), String::from("o")),
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("サ"), String::from("sa")),
|
|
|
|
|
|
(String::from("シ"), String::from("shi")),
|
|
|
|
|
|
(String::from("ス"), String::from("su")),
|
|
|
|
|
|
(String::from("セ"), String::from("se")),
|
|
|
|
|
|
(String::from("ソ"), String::from("so")),
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("カ"), String::from("ka")),
|
|
|
|
|
|
(String::from("キ"), String::from("ki")),
|
|
|
|
|
|
(String::from("ク"), String::from("ku")),
|
|
|
|
|
|
(String::from("ケ"), String::from("ke")),
|
|
|
|
|
|
(String::from("コ"), String::from("ko")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «та» (タチツテト)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("タ"), String::from("ta")),
|
|
|
|
|
|
(String::from("チ"), String::from("chi")),
|
|
|
|
|
|
(String::from("ツ"), String::from("tsu")),
|
|
|
|
|
|
(String::from("テ"), String::from("te")),
|
|
|
|
|
|
(String::from("ト"), String::from("to")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «на» (ナニヌネノ)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("ナ"), String::from("na")),
|
|
|
|
|
|
(String::from("ニ"), String::from("ni")),
|
|
|
|
|
|
(String::from("ヌ"), String::from("nu")),
|
|
|
|
|
|
(String::from("ネ"), String::from("ne")),
|
|
|
|
|
|
(String::from("ノ"), String::from("no")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «ха» (ハヒフヘホ)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("ハ"), String::from("ha")),
|
|
|
|
|
|
(String::from("ヒ"), String::from("hi")),
|
|
|
|
|
|
(String::from("フ"), String::from("fu")),
|
|
|
|
|
|
(String::from("ヘ"), String::from("he")),
|
|
|
|
|
|
(String::from("ホ"), String::from("ho")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «ма» (マミムメモ)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("マ"), String::from("ma")),
|
|
|
|
|
|
(String::from("ミ"), String::from("mi")),
|
|
|
|
|
|
(String::from("ム"), String::from("mu")),
|
|
|
|
|
|
(String::from("メ"), String::from("me")),
|
|
|
|
|
|
(String::from("モ"), String::from("mo")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «я» (ヤユヨ) — только 3 символа
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("ヤ"), String::from("ya")),
|
|
|
|
|
|
(String::from("ユ"), String::from("yu")),
|
|
|
|
|
|
(String::from("ヨ"), String::from("yo")),
|
|
|
|
|
|
],
|
|
|
|
|
|
// Ряд «ра» (ラリルレロ)
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("ラ"), String::from("ra")),
|
|
|
|
|
|
(String::from("リ"), String::from("ri")),
|
|
|
|
|
|
(String::from("ル"), String::from("ru")),
|
|
|
|
|
|
(String::from("レ"), String::from("re")),
|
|
|
|
|
|
(String::from("ロ"), String::from("ro")),
|
|
|
|
|
|
],
|
|
|
|
|
|
vec![
|
|
|
|
|
|
(String::from("ワ"), String::from("wa")),
|
|
|
|
|
|
(String::from("ヲ"), String::from("wo")),
|
|
|
|
|
|
(String::from("ン"), String::from("n")),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
include_map: [true; 10],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 17:04:48 +03:00
|
|
|
|
pub fn list(&self) -> Vec<(String, String)> {
|
2026-02-10 12:12:16 +03:00
|
|
|
|
let mut current_set: Vec<(String, String)> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
for i in 0..10 {
|
|
|
|
|
|
if self.include_map[i] {
|
|
|
|
|
|
self.dictionary[i].iter().for_each(|v| {
|
|
|
|
|
|
current_set.push(v.clone());
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-10 17:30:43 +03:00
|
|
|
|
|
2026-02-10 17:04:48 +03:00
|
|
|
|
current_set
|
2026-02-10 12:12:16 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
impl Default for KanaSet {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
KanaSet::hiragana()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
impl PartialEq<Self> for KanaSet {
|
|
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
|
|
self.name == other.name
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-18 12:47:06 +03:00
|
|
|
|
#[derive(Clone, Debug)]
|
2026-05-05 12:02:59 +03:00
|
|
|
|
pub struct WordData {
|
2026-08-18 12:47:06 +03:00
|
|
|
|
pub id: Id,
|
2026-04-13 22:25:58 +03:00
|
|
|
|
pub key: String,
|
|
|
|
|
|
pub value: String,
|
|
|
|
|
|
pub tags: String,
|
|
|
|
|
|
pub additional: HashMap<String, String>,
|
2026-08-18 12:47:06 +03:00
|
|
|
|
pub group_id: Id,
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
2026-05-05 12:02:59 +03:00
|
|
|
|
impl WordData {
|
2026-04-13 22:25:58 +03:00
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
|
Self {
|
2026-08-18 12:58:02 +03:00
|
|
|
|
id: INVALID_ID,
|
2026-04-13 22:25:58 +03:00
|
|
|
|
key: String::new(),
|
|
|
|
|
|
value: String::new(),
|
|
|
|
|
|
tags: String::new(),
|
|
|
|
|
|
additional: Default::default(),
|
2026-08-18 12:47:06 +03:00
|
|
|
|
group_id: 1.into(),
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-05 14:26:34 +03:00
|
|
|
|
#[derive(Clone)]
|
2026-05-12 17:12:03 +03:00
|
|
|
|
pub struct WordGroup {
|
2026-08-18 12:47:06 +03:00
|
|
|
|
pub id: Id,
|
2026-05-05 12:02:59 +03:00
|
|
|
|
pub name: String,
|
|
|
|
|
|
}
|
2026-04-14 15:19:46 +03:00
|
|
|
|
#[derive(Clone, PartialEq)]
|
2026-04-13 22:25:58 +03:00
|
|
|
|
pub struct CardStatistics {
|
2026-08-18 12:47:06 +03:00
|
|
|
|
pub id: Id,
|
|
|
|
|
|
pub word_id: Id,
|
|
|
|
|
|
pub set_id: Id,
|
2026-04-13 22:25:58 +03:00
|
|
|
|
pub last_open: DateTime<Utc>,
|
2026-07-29 12:57:46 +03:00
|
|
|
|
pub score: u8,
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
impl CardStatistics {
|
2026-04-14 15:19:46 +03:00
|
|
|
|
pub fn update(&mut self, status: WordOpenMode) {
|
2026-04-13 22:25:58 +03:00
|
|
|
|
match status {
|
|
|
|
|
|
WordOpenMode::Easy => {
|
2026-07-29 12:57:46 +03:00
|
|
|
|
self.score = (self.calculated_score() + 5.0).round() as u8;
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
2026-07-29 12:57:46 +03:00
|
|
|
|
WordOpenMode::Ok => self.score = (self.calculated_score() + 2.0).round() as u8,
|
2026-04-13 22:25:58 +03:00
|
|
|
|
WordOpenMode::Hard => {
|
2026-07-29 12:57:46 +03:00
|
|
|
|
self.score = (self.calculated_score() * 0.75).round() as u8;
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
WordOpenMode::None => {
|
2026-07-29 12:57:46 +03:00
|
|
|
|
self.score = (self.calculated_score() * 0.4) as u8;
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 13:22:59 +03:00
|
|
|
|
self.score = self.score.clamp(1, MAX_SCORE);
|
2026-04-16 09:56:57 +03:00
|
|
|
|
self.last_open = Utc::now();
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-08-01 19:36:43 +03:00
|
|
|
|
(self.score as f32 * multiplier).max(1.0)
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-25 12:23:32 +03:00
|
|
|
|
#[derive(Clone, Copy)]
|
2026-04-13 22:25:58 +03:00
|
|
|
|
pub enum WordOpenMode {
|
|
|
|
|
|
Easy,
|
|
|
|
|
|
Ok,
|
|
|
|
|
|
Hard,
|
|
|
|
|
|
None,
|
|
|
|
|
|
}
|
|
|
|
|
|
#[derive(Clone)]
|
2026-08-18 12:14:35 +03:00
|
|
|
|
pub struct DeckData {
|
2026-05-05 12:02:59 +03:00
|
|
|
|
words: Vec<WordData>,
|
2026-04-13 22:25:58 +03:00
|
|
|
|
set: Vec<CardStatistics>,
|
2026-04-14 15:19:46 +03:00
|
|
|
|
current_word_index: Option<usize>,
|
2026-04-17 13:06:37 +03:00
|
|
|
|
state: Arc<Mutex<AppState>>,
|
2026-05-25 12:23:32 +03:00
|
|
|
|
order_module: OrderModule,
|
2026-08-18 12:14:35 +03:00
|
|
|
|
settings: DeckSettings,
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
2026-08-18 12:14:35 +03:00
|
|
|
|
impl DeckData {
|
|
|
|
|
|
pub fn new(settings: &DeckSettings, state: Arc<Mutex<AppState>>) -> Self {
|
2026-04-14 15:19:46 +03:00
|
|
|
|
let state_for = state.clone();
|
2026-04-13 22:25:58 +03:00
|
|
|
|
let state_locked = state.lock().unwrap();
|
|
|
|
|
|
|
2026-08-18 15:16:51 +03:00
|
|
|
|
let mut current_set = load_stats_of_deck(settings, &state_locked.connection);
|
2026-08-15 21:32:24 +03:00
|
|
|
|
let last_list: Vec<_> = settings
|
|
|
|
|
|
.get_word_list(&state_locked)
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|w| state_locked.dictionary.get(*w).unwrap())
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.collect();
|
2026-08-18 12:47:06 +03:00
|
|
|
|
let word_ids = last_list.iter().map(|l| l.id).collect::<Vec<Id>>();
|
|
|
|
|
|
|
2026-04-15 07:53:32 +03:00
|
|
|
|
let mut index = 0;
|
|
|
|
|
|
for stat in current_set.clone() {
|
|
|
|
|
|
if !word_ids.contains(&stat.word_id) {
|
|
|
|
|
|
delete_stat(&stat, &state_locked.connection);
|
|
|
|
|
|
current_set.remove(index);
|
2026-05-12 17:12:03 +03:00
|
|
|
|
} else {
|
2026-05-12 16:57:05 +03:00
|
|
|
|
index += 1;
|
2026-04-15 07:53:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 22:25:58 +03:00
|
|
|
|
Self {
|
|
|
|
|
|
set: current_set,
|
2026-04-14 15:19:46 +03:00
|
|
|
|
words: last_list,
|
|
|
|
|
|
current_word_index: None,
|
2026-04-17 13:06:37 +03:00
|
|
|
|
state: state_for,
|
2026-05-25 12:23:32 +03:00
|
|
|
|
order_module: match settings.open_mode {
|
2026-08-15 20:16:48 +03:00
|
|
|
|
OrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()),
|
|
|
|
|
|
OrderMode::TrainWorstFirst => {
|
2026-07-29 14:24:40 +03:00
|
|
|
|
OrderModule::WorstWordsSRS(WorstWordsSRSModule::new())
|
|
|
|
|
|
}
|
2026-08-15 20:16:48 +03:00
|
|
|
|
OrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()),
|
2026-05-25 12:23:32 +03:00
|
|
|
|
},
|
2026-07-29 12:57:46 +03:00
|
|
|
|
settings: settings.clone(),
|
2026-04-14 15:19:46 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-05 12:02:59 +03:00
|
|
|
|
pub fn next(&mut self) -> (WordData, CardStatistics) {
|
2026-05-25 12:23:32 +03:00
|
|
|
|
let index = match self.order_module.clone() {
|
|
|
|
|
|
OrderModule::SemiRandomSRS(mut module) => {
|
2026-08-15 13:22:59 +03:00
|
|
|
|
if !module.initialized {
|
2026-05-25 12:23:32 +03:00
|
|
|
|
module.init(self)
|
|
|
|
|
|
}
|
|
|
|
|
|
let index = module.next(self);
|
|
|
|
|
|
self.order_module = OrderModule::SemiRandomSRS(module);
|
|
|
|
|
|
index
|
|
|
|
|
|
}
|
|
|
|
|
|
OrderModule::RandomSRS(mut module) => {
|
2026-08-15 13:22:59 +03:00
|
|
|
|
if !module.initialized {
|
2026-05-25 12:23:32 +03:00
|
|
|
|
module.init(self)
|
|
|
|
|
|
}
|
|
|
|
|
|
let index = module.next(self);
|
|
|
|
|
|
self.order_module = OrderModule::RandomSRS(module);
|
|
|
|
|
|
index
|
|
|
|
|
|
}
|
2026-07-29 14:24:40 +03:00
|
|
|
|
OrderModule::WorstWordsSRS(mut module) => {
|
2026-08-15 13:22:59 +03:00
|
|
|
|
if !module.initialized {
|
2026-08-01 19:43:07 +03:00
|
|
|
|
module.init(self)
|
|
|
|
|
|
}
|
|
|
|
|
|
let index = module.next(self);
|
|
|
|
|
|
self.order_module = OrderModule::WorstWordsSRS(module);
|
|
|
|
|
|
index
|
|
|
|
|
|
}
|
2026-05-25 12:23:32 +03:00
|
|
|
|
};
|
2026-04-17 13:06:37 +03:00
|
|
|
|
|
2026-04-14 15:19:46 +03:00
|
|
|
|
self.current_word_index = Some(index);
|
2026-05-05 12:02:59 +03:00
|
|
|
|
(self.words[index].clone(), self.set[index].clone())
|
2026-04-14 15:19:46 +03:00
|
|
|
|
}
|
|
|
|
|
|
pub fn open(&mut self, status: WordOpenMode) {
|
2026-08-15 13:22:59 +03:00
|
|
|
|
if self.current_word_index.is_none() {
|
2026-04-14 15:19:46 +03:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-25 12:23:32 +03:00
|
|
|
|
let index = self.current_word_index.unwrap();
|
2026-04-14 15:19:46 +03:00
|
|
|
|
|
2026-05-25 12:23:32 +03:00
|
|
|
|
let word = &mut self.set[index];
|
2026-07-29 12:57:46 +03:00
|
|
|
|
let old_score = word.score;
|
2026-04-14 15:19:46 +03:00
|
|
|
|
word.update(status);
|
2026-07-29 12:57:46 +03:00
|
|
|
|
let new_score = word.score;
|
2026-05-25 12:23:32 +03:00
|
|
|
|
|
|
|
|
|
|
match self.order_module.clone() {
|
|
|
|
|
|
OrderModule::SemiRandomSRS(mut module) => {
|
|
|
|
|
|
module.open(status, index, word.clone());
|
|
|
|
|
|
self.order_module = OrderModule::SemiRandomSRS(module);
|
|
|
|
|
|
}
|
|
|
|
|
|
OrderModule::RandomSRS(mut module) => {
|
|
|
|
|
|
module.open(status, index, word.clone());
|
|
|
|
|
|
self.order_module = OrderModule::RandomSRS(module);
|
|
|
|
|
|
}
|
2026-07-29 14:24:40 +03:00
|
|
|
|
OrderModule::WorstWordsSRS(mut module) => {
|
2026-08-01 19:43:07 +03:00
|
|
|
|
module.open(status, index, word.clone());
|
|
|
|
|
|
self.order_module = OrderModule::WorstWordsSRS(module);
|
|
|
|
|
|
}
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
2026-07-29 12:57:46 +03:00
|
|
|
|
update_stat_score(word, &self.state.lock().unwrap().connection);
|
2026-07-29 14:24:40 +03:00
|
|
|
|
push_note(
|
|
|
|
|
|
self.settings.id,
|
|
|
|
|
|
HistoryItem {
|
|
|
|
|
|
timestamp: Utc::now(),
|
2026-08-18 12:47:06 +03:00
|
|
|
|
word_id: word.word_id.into(),
|
2026-07-29 14:24:40 +03:00
|
|
|
|
mode: WordOpenMode::Easy,
|
|
|
|
|
|
before: old_score,
|
|
|
|
|
|
after: new_score,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
pub fn len(&self) -> usize {
|
|
|
|
|
|
self.set.len()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
#[derive(Clone, PartialEq, Copy, Eq)]
|
2026-08-15 20:16:48 +03:00
|
|
|
|
pub enum OrderMode {
|
2026-05-25 12:23:32 +03:00
|
|
|
|
Default,
|
2026-07-29 14:24:40 +03:00
|
|
|
|
TrainWorstFirst,
|
2026-05-25 12:23:32 +03:00
|
|
|
|
FullRandom,
|
|
|
|
|
|
}
|
2026-08-18 12:14:35 +03:00
|
|
|
|
#[derive(Clone, Copy, Eq, PartialEq, Default)]
|
2026-08-15 21:32:24 +03:00
|
|
|
|
pub enum AppendMode {
|
2026-08-15 20:16:48 +03:00
|
|
|
|
Full,
|
2026-08-18 12:14:35 +03:00
|
|
|
|
#[default]
|
2026-08-15 21:32:24 +03:00
|
|
|
|
Manual,
|
2026-08-15 20:16:48 +03:00
|
|
|
|
}
|
2026-05-25 12:23:32 +03:00
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
|
enum OrderModule {
|
|
|
|
|
|
SemiRandomSRS(SemiRandomSRSModule),
|
|
|
|
|
|
RandomSRS(RandomSRSModule),
|
2026-07-29 14:24:40 +03:00
|
|
|
|
WorstWordsSRS(WorstWordsSRSModule),
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
trait SRSModule {
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn next(&mut self, set: &mut DeckData) -> usize;
|
2026-05-25 12:23:32 +03:00
|
|
|
|
fn open(&mut self, status: WordOpenMode, index: usize, updated_word: CardStatistics);
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn init(&mut self, set: &mut DeckData);
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
|
struct RandomSRSModule {
|
2026-06-15 17:06:02 +03:00
|
|
|
|
basket: Vec<usize>,
|
|
|
|
|
|
initialized: bool,
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
impl RandomSRSModule {
|
|
|
|
|
|
fn new() -> RandomSRSModule {
|
2026-07-29 14:24:40 +03:00
|
|
|
|
Self {
|
2026-06-15 17:06:02 +03:00
|
|
|
|
basket: vec![],
|
|
|
|
|
|
initialized: false,
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
impl SRSModule for RandomSRSModule {
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn next(&mut self, set: &mut DeckData) -> usize {
|
2026-06-15 17:06:02 +03:00
|
|
|
|
if self.basket.is_empty() {
|
|
|
|
|
|
self.basket = (0..set.words.len()).collect::<Vec<usize>>();
|
|
|
|
|
|
self.basket.shuffle(&mut rand::rng())
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 17:06:02 +03:00
|
|
|
|
self.basket.pop().unwrap()
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn open(&mut self, _: WordOpenMode, _: usize, _: CardStatistics) {}
|
|
|
|
|
|
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn init(&mut self, set: &mut DeckData) {
|
2026-06-15 17:06:02 +03:00
|
|
|
|
self.initialized = true;
|
|
|
|
|
|
self.basket = (0..set.words.len()).collect::<Vec<usize>>();
|
|
|
|
|
|
self.basket.shuffle(&mut rand::rng())
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
|
struct SemiRandomSRSModule {
|
|
|
|
|
|
history: Vec<usize>,
|
|
|
|
|
|
last_weights: WeightedIndex<f32>,
|
|
|
|
|
|
generator: ThreadRng,
|
2026-06-15 17:06:02 +03:00
|
|
|
|
initialized: bool,
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
impl SemiRandomSRSModule {
|
|
|
|
|
|
fn new() -> SemiRandomSRSModule {
|
|
|
|
|
|
SemiRandomSRSModule {
|
|
|
|
|
|
history: vec![],
|
|
|
|
|
|
last_weights: WeightedIndex::new([1.0]).unwrap(),
|
|
|
|
|
|
generator: rng(),
|
2026-06-15 17:06:02 +03:00
|
|
|
|
initialized: false,
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
impl SRSModule for SemiRandomSRSModule {
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn next(&mut self, set: &mut DeckData) -> usize {
|
2026-05-25 12:23:32 +03:00
|
|
|
|
let index = self.last_weights.sample(&mut self.generator);
|
|
|
|
|
|
|
|
|
|
|
|
if self.history.contains(&index) {
|
|
|
|
|
|
return self.next(set);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if self.history.len() == self.history_len(set) {
|
|
|
|
|
|
self.history.remove(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.history.push(index);
|
|
|
|
|
|
|
|
|
|
|
|
index
|
|
|
|
|
|
}
|
2026-06-15 17:06:02 +03:00
|
|
|
|
fn open(&mut self, _: WordOpenMode, index: usize, word: CardStatistics) {
|
2026-04-20 10:13:39 +03:00
|
|
|
|
let new_weight = (100.0 / word.calculated_score()).powf(2.0);
|
2026-05-12 17:12:03 +03:00
|
|
|
|
self.last_weights
|
2026-05-25 12:23:32 +03:00
|
|
|
|
.update_weights(&[(index, &new_weight)])
|
2026-05-12 17:12:03 +03:00
|
|
|
|
.unwrap();
|
2026-04-13 22:25:58 +03:00
|
|
|
|
}
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn init(&mut self, set: &mut DeckData) {
|
2026-06-15 17:06:02 +03:00
|
|
|
|
self.initialized = true;
|
2026-05-25 12:23:32 +03:00
|
|
|
|
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 {
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn history_len(&self, set: &DeckData) -> usize {
|
|
|
|
|
|
((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN)
|
2026-04-17 13:06:37 +03:00
|
|
|
|
}
|
2026-05-25 12:23:32 +03:00
|
|
|
|
}
|
2026-07-29 14:24:40 +03:00
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
|
struct WorstWordsSRSModule {
|
|
|
|
|
|
initialized: bool,
|
2026-08-01 19:36:43 +03:00
|
|
|
|
pool_size: usize,
|
|
|
|
|
|
rounds_remaining: u8,
|
|
|
|
|
|
pool: Vec<usize>,
|
|
|
|
|
|
queue: Vec<usize>,
|
|
|
|
|
|
rounds_count: u8,
|
2026-07-29 14:24:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
impl SRSModule for WorstWordsSRSModule {
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn next(&mut self, set: &mut DeckData) -> usize {
|
2026-08-01 19:36:43 +03:00
|
|
|
|
if self.rounds_remaining == 0 {
|
|
|
|
|
|
self.fill_pool(set);
|
|
|
|
|
|
self.rounds_remaining = self.rounds_count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if self.queue.is_empty() {
|
|
|
|
|
|
self.queue.append(&mut self.pool.clone());
|
|
|
|
|
|
self.rounds_remaining -= 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.queue.pop().unwrap()
|
2026-07-29 14:24:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 19:43:07 +03:00
|
|
|
|
fn open(&mut self, _: WordOpenMode, _: usize, _: CardStatistics) {}
|
2026-07-29 14:24:40 +03:00
|
|
|
|
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn init(&mut self, _: &mut DeckData) {
|
2026-07-29 14:24:40 +03:00
|
|
|
|
self.initialized = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
impl WorstWordsSRSModule {
|
|
|
|
|
|
fn new() -> WorstWordsSRSModule {
|
|
|
|
|
|
WorstWordsSRSModule {
|
|
|
|
|
|
initialized: false,
|
2026-08-01 19:36:43 +03:00
|
|
|
|
pool_size: 15,
|
|
|
|
|
|
rounds_count: 2,
|
|
|
|
|
|
rounds_remaining: 0,
|
2026-07-29 14:24:40 +03:00
|
|
|
|
pool: vec![],
|
2026-08-01 19:36:43 +03:00
|
|
|
|
queue: vec![],
|
2026-07-29 14:24:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-18 12:14:35 +03:00
|
|
|
|
fn fill_pool(&mut self, set: &DeckData) {
|
2026-08-01 19:43:07 +03:00
|
|
|
|
let mut sorted = set
|
|
|
|
|
|
.set
|
|
|
|
|
|
.clone()
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.zip(0..set.set.len())
|
|
|
|
|
|
.collect::<Vec<_>>();
|
2026-08-01 19:36:43 +03:00
|
|
|
|
sorted.sort_by_key(|c| c.0.score);
|
2026-08-01 19:43:07 +03:00
|
|
|
|
let mut worst = sorted[0..self.pool_size]
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|(_, index)| *index)
|
|
|
|
|
|
.collect::<Vec<usize>>();
|
2026-08-01 19:36:43 +03:00
|
|
|
|
worst.shuffle(&mut rand::rng());
|
|
|
|
|
|
self.pool = worst;
|
|
|
|
|
|
}
|
2026-07-29 14:24:40 +03:00
|
|
|
|
}
|
2026-08-15 20:16:48 +03:00
|
|
|
|
#[derive(Clone)]
|
2026-08-18 12:14:35 +03:00
|
|
|
|
pub struct DeckSettings {
|
2026-08-18 12:47:06 +03:00
|
|
|
|
pub id: Id,
|
2026-08-15 20:16:48 +03:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
2026-08-18 12:14:35 +03:00
|
|
|
|
impl DeckSettings {
|
|
|
|
|
|
pub(crate) fn with_name(name: String) -> DeckSettings {
|
|
|
|
|
|
DeckSettings {
|
2026-08-18 12:47:06 +03:00
|
|
|
|
id: 0.into(),
|
2026-08-15 20:16:48 +03:00
|
|
|
|
name,
|
|
|
|
|
|
forward: "".to_string(),
|
|
|
|
|
|
backward: "".to_string(),
|
|
|
|
|
|
filter: "true".to_string(),
|
|
|
|
|
|
count: None,
|
|
|
|
|
|
worst_words_list: None,
|
|
|
|
|
|
open_mode: OrderMode::Default,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-18 17:45:50 +03:00
|
|
|
|
pub fn check_filter(&self) -> bool {
|
|
|
|
|
|
let time = Instant::now();
|
2026-08-15 20:16:48 +03:00
|
|
|
|
let engine = Engine::new();
|
|
|
|
|
|
let ast = engine.compile(&self.filter);
|
2026-08-18 17:45:50 +03:00
|
|
|
|
println!("Compile time is {:?}", time.elapsed());
|
2026-08-15 20:16:48 +03:00
|
|
|
|
ast.is_ok()
|
|
|
|
|
|
}
|
2026-08-15 21:32:24 +03:00
|
|
|
|
pub fn get_word_list(&self, state: &AppState) -> Vec<usize> {
|
2026-08-15 20:16:48 +03:00
|
|
|
|
let time = Instant::now();
|
|
|
|
|
|
let mut list = vec![];
|
|
|
|
|
|
let engine = Engine::new();
|
|
|
|
|
|
|
|
|
|
|
|
let ast = engine.compile(&self.filter);
|
|
|
|
|
|
if ast.is_err() {
|
|
|
|
|
|
return list;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let ast = ast.unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
let groups = &state.word_groups;
|
|
|
|
|
|
|
2026-08-15 21:32:24 +03:00
|
|
|
|
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
|
2026-08-18 12:47:06 +03:00
|
|
|
|
.push_constant("id", word.id.0)
|
2026-08-15 21:32:24 +03:00
|
|
|
|
.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();
|
2026-08-15 20:16:48 +03:00
|
|
|
|
|
|
|
|
|
|
println!("Collecting available words is {:?}", time.elapsed());
|
|
|
|
|
|
|
|
|
|
|
|
list
|
|
|
|
|
|
}
|
|
|
|
|
|
pub fn require_speech(&self) -> bool {
|
|
|
|
|
|
self.forward == "speech" || self.backward == "speech"
|
|
|
|
|
|
}
|
2026-08-18 17:31:19 +03:00
|
|
|
|
}
|