repetition works

This commit is contained in:
2026-04-14 15:19:46 +03:00
parent a1217f90ec
commit 13156da39f
5 changed files with 281 additions and 60 deletions
+35 -24
View File
@@ -1,10 +1,6 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use iced::time::now;
use rusqlite::{Connection, ToSql};
use rusqlite::types::ToSqlOutput;
use crate::lang::{CardSet, CardStatistics, DictionaryElement};
use crate::lang::CardStatistics;
use crate::repetitions::CardSetSettings;
use rusqlite::Connection;
pub fn load_stats_of_set(set: &CardSetSettings, connection: &Connection) -> Vec<CardStatistics> {
let mut stmt = connection.prepare("SELECT id, word_id, score, last_opened FROM card_stats WHERE set_id = ?1").unwrap();
@@ -12,8 +8,9 @@ pub fn load_stats_of_set(set: &CardSetSettings, connection: &Connection) -> Vec<
Ok(CardStatistics {
id: row.get(0)?,
word_id: row.get(1)?,
set_id: set.id,
score: row.get(2)?,
last_open: row.get(2)?,
last_open: row.get(3)?,
})
}).unwrap();
@@ -25,50 +22,64 @@ pub fn load_stats_of_set(set: &CardSetSettings, connection: &Connection) -> Vec<
buffer
}
pub fn add_set(set: &mut CardSetSettings, connection: &Connection) {
pub fn add_stat(stat: &mut CardStatistics, connection: &Connection) {
let index = connection
.query_row(
"INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id",
"INSERT INTO card_stats (word_id, set_id, score, last_opened) VALUES (?1, ?2, ?3, ?4) RETURNING id",
(
&set.name,
&set.forward,
&set.backward,
&set.filter,
&stat.word_id,
&stat.set_id,
&stat.score,
&stat.last_open,
),
|row| row.get(0)
)
.unwrap_or_else(|e| {println!("{}", e); 0});
set.id = index;
stat.id = index;
}
pub fn update_card_set(set: &mut CardSetSettings, connection: &Connection){
if set.id == 0 {
add_set(set, &connection);
pub fn update_stat(stat: &mut CardStatistics, connection: &Connection){
if stat.id == 0 {
add_stat(stat, &connection);
}
else {
connection
.execute(
"UPDATE card_set SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5",
"UPDATE card_stats SET word_id = ?1, set_id = ?2, score = ?3, last_opened = ?4 WHERE id = ?5",
(
&set.name,
&set.forward,
&set.backward,
&set.filter,
&set.id
&stat.word_id,
&stat.set_id,
&stat.score,
&stat.last_open,
&stat.id
),
)
.unwrap_or_else(|e| {println!("{}", e); 0});
}
}
pub fn update_stat_score(stat: &CardStatistics, connection: &Connection){
connection
.execute(
"UPDATE card_stats SET score = ?1, last_opened = ?2 WHERE id = ?3",
(
&stat.score,
&stat.last_open,
&stat.id
),
)
.unwrap_or_else(|e| {println!("{}", e); 0});
}
pub fn delete_set(set: &CardSetSettings, connection: &Connection) {
if set.id == 0 {
return;
}
connection
.execute("DELETE FROM card_set WHERE id = ?1", (&set.id,))
.execute("DELETE FROM card_stats WHERE id = ?1", (&set.id,))
.unwrap_or_else(|e| {
println!("{}", e);
0
+64 -13
View File
@@ -1,10 +1,14 @@
use crate::data_provider::card_stats::{add_stat, load_stats_of_set, update_stat_score};
use crate::repetitions::CardSetSettings;
use crate::AppState;
use chrono::{DateTime, Utc};
use rand::distr::weighted::WeightedIndex;
use rand::distr::Distribution;
use rand::rng;
use rand::rngs::ThreadRng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::data_provider::card_stats::load_stats_of_set;
const MAX_SCORE: u8 = 25_u8;
const FADE_PER_DAY: f32 = 0.95;
@@ -242,29 +246,30 @@ impl DictionaryElement {
}
}
#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub struct CardStatistics {
pub id: u32,
pub word_id: u32,
pub set_id: u32,
pub last_open: DateTime<Utc>,
pub score: u8,
}
impl CardStatistics {
pub fn open(&mut self, status: WordOpenMode) {
pub fn update(&mut self, status: WordOpenMode) {
self.last_open = Utc::now();
match status {
WordOpenMode::Easy => {
self.score += 5;
self.score = self.calculated_score().round() as u8 + 5;
}
WordOpenMode::Ok => {
self.score += 3;
self.score += self.calculated_score().round() as u8 + 3;
}
WordOpenMode::Hard => {
self.score -= 1;
self.score = self.calculated_score().round() as u8 - 1;
}
WordOpenMode::None => {
self.score /= 2;
self.score = (self.calculated_score() * 0.5) as u8;
}
}
@@ -279,10 +284,11 @@ impl CardStatistics {
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
(1.0 / self.score as f32) * multiplier
}
}
#[derive(Clone)]
pub enum WordOpenMode {
Easy,
Ok,
@@ -292,22 +298,67 @@ pub enum WordOpenMode {
#[derive(Clone)]
pub struct CardSet {
words: Vec<DictionaryElement>,
set: Vec<CardStatistics>,
last_weights: Vec<f32>,
last_weights: WeightedIndex<f32>,
current_word_index: Option<usize>,
generator: ThreadRng,
state: Arc<Mutex<AppState>>
}
impl CardSet {
pub fn new(settings: CardSetSettings, state: Arc<Mutex<AppState>>) -> Self {
pub fn new(settings: &CardSetSettings, state: Arc<Mutex<AppState>>) -> Self {
let state_for = state.clone();
let state_locked = state.lock().unwrap();
let current_set = load_stats_of_set(&settings, &state_locked.connection);
let mut current_set = load_stats_of_set(&settings, &state_locked.connection);
let last_list = settings.get_word_list(&state_locked);
let saved_ids = current_set.iter().map(|l| l.word_id).collect::<Vec<u32>>();
for word in &last_list {
if !saved_ids.contains(&word.id) {
let mut new_statistic = CardStatistics {
id: 0,
word_id: word.id.clone(),
last_open: Utc::now(),
score: 1,
set_id: settings.id.clone(),
};
let weights = current_set.iter().map(|x| x.calculated_score()).collect();
add_stat(&mut new_statistic, &state_locked.connection);
current_set.push(new_statistic);
}
}
let indexes = WeightedIndex::new(current_set.iter().map(|s| 1.0 / s.score as f32)).unwrap();
Self {
set: current_set,
last_weights: weights,
words: last_list,
last_weights: indexes,
current_word_index: None,
generator: rng(),
state: state_for
}
}
pub fn next(&mut self) -> DictionaryElement {
let index = self.last_weights.sample(&mut self.generator);
self.current_word_index = Some(index);
self.words[index].clone()
}
pub fn open(&mut self, status: WordOpenMode) {
if let None = self.current_word_index {
return;
}
let word = &mut self.set[self.current_word_index.unwrap()];
word.update(status);
let new_weight = word.calculated_score();
self.last_weights.update_weights(&[(self.current_word_index.unwrap(), &new_weight)]).unwrap();
{
update_stat_score(word, &self.state.lock().unwrap().connection)
}
}
}
+30 -4
View File
@@ -24,13 +24,15 @@ use crate::Page::{
Dictionary, DictionaryQuiz, Quiz, Randomizer, Repetition, Repetitions, Selector, Writing,
};
use iced::widget::text;
use iced::Element;
use iced::{keyboard, Element, Program, Subscription};
use iced::{Font, Task};
use rusqlite::Connection;
use std::sync::{Arc, Mutex};
use iced::keyboard::Event;
use crate::RootMessage::Keyboard;
fn main() -> iced::Result {
iced::application(ScreenState::boot, ScreenState::update, ScreenState::view)
iced::application(ScreenState::boot, ScreenState::update, ScreenState::view).subscription(subscription)
.title("Kana learn app")
.font(include_bytes!("../noto.ttf"))
.default_font(Font::with_name("Noto Sans JP"))
@@ -38,7 +40,11 @@ fn main() -> iced::Result {
run()
}
#[derive(Debug, Clone)]
fn subscription(_state: &ScreenState) -> Subscription<RootMessage> {
keyboard::listen().map(|e| Keyboard(e))
}
#[derive(Clone)]
pub enum RootMessage {
Selector(SelectorMessage),
Quiz(QuizMessage),
@@ -48,6 +54,7 @@ pub enum RootMessage {
Randomizer(RandomizerMessage),
Repetitions(RepetitionsMessage),
Repetition(RepetitionMessage),
Keyboard(keyboard::Event),
}
enum Page {
@@ -93,6 +100,10 @@ impl ScreenState {
(ScreenState::default(), Task::none())
}
pub fn update(&mut self, message: RootMessage) -> Task<RootMessage> {
if let Keyboard(e) = message {
return self.handle_keyboard(e);
}
state_update!(
message,
self.stack,
@@ -107,7 +118,6 @@ impl ScreenState {
);
Task::none()
}
pub fn view(&self) -> Element<'_, RootMessage> {
view_navigation!(
self.stack,
@@ -121,8 +131,19 @@ impl ScreenState {
Repetition
)
}
fn handle_keyboard(&mut self, message: Event) -> Task<RootMessage> {
let page = self.stack.last_mut().unwrap();
match page {
Repetition(page) => page.press(&message),
_ => {}
}
Task::none()
}
}
#[macro_export]
macro_rules! view_navigation {
($stack:expr, $($e:ident), *) => {
@@ -146,6 +167,7 @@ macro_rules! state_update {
}
}
)*
_ => {}
}
}
}
@@ -168,3 +190,7 @@ macro_rules! message_navigation {
trait NavigatedPage<T> {
fn navigate(&self, message: &T) -> Option<Page>;
}
pub trait KeyPressedPage {
fn press(&mut self, message: &keyboard::Event);
}
+142 -14
View File
@@ -1,15 +1,22 @@
use std::sync::{Arc, Mutex};
use crate::Page::PreviousPage;
use crate::{AppState, NavigatedPage, Page, RootMessage};
use iced::widget::{button, container};
use iced::{Element, Fill, Left, Task};
use crate::lang::CardSet;
use crate::lang::{CardSet, DictionaryElement, WordOpenMode};
use crate::repetitions::CardSetSettings;
use crate::Page::PreviousPage;
use crate::{AppState, KeyPressedPage, NavigatedPage, Page, RootMessage};
use iced::alignment::Horizontal::Center;
use iced::keyboard::key::Physical::Code;
use iced::widget::container::rounded_box;
use iced::widget::space::vertical;
use iced::widget::{button, column, container, row, space, text};
use iced::{alignment, keyboard, Element, Fill, Left, Task};
use rusqlite::fallible_iterator::FallibleIterator;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct RepetitionState {
pub set: CardSetSettings,
pub settings: CardSetSettings,
pub set: CardSet,
pub state: Arc<Mutex<AppState>>,
current_word: DictionaryElement,
open: bool,
}
impl NavigatedPage<RepetitionMessage> for RepetitionState {
@@ -24,34 +31,155 @@ impl NavigatedPage<RepetitionMessage> for RepetitionState {
impl RepetitionState {
pub(crate) fn new(set: CardSetSettings, state: Arc<Mutex<AppState>>) -> RepetitionState {
let mut card_set = CardSet::new(&set, state.clone());
let word = card_set.next();
RepetitionState {
set,
state
settings: set,
set: card_set,
state,
current_word: word,
open: false,
}
}
}
impl RepetitionState {
pub fn update(&mut self, message: RepetitionMessage) -> Task<RootMessage> {
match message {
RepetitionMessage::Back => {}
RepetitionMessage::Next => {self.next()}
RepetitionMessage::Answer(m) => {self.answer(m)}
}
Task::none()
}
fn next(&mut self) {
if self.open {
self.set.open(WordOpenMode::None);
self.current_word = self.set.next();
return;
} else {
self.open = true;
}
}
fn answer(&mut self, mode: WordOpenMode) {
if !self.open {
return;
}
self.set.open(mode);
self.open = false;
self.current_word = self.set.next();
}
pub fn view(&self) -> Element<'_, RepetitionMessage> {
container(
iced::widget::column![
button("Назад").on_press(RepetitionMessage::Back),
].align_x(Left).width(Fill)
column![
container(self.draw_forward())
.width(Fill)
.height(Fill)
.align_x(Center)
.align_y(alignment::Vertical::Center),
container(vertical().height(5)).width(Fill).padding(5).style(rounded_box),
container(self.draw_backward())
.width(Fill)
.height(Fill)
.align_x(Center)
.align_y(alignment::Vertical::Center),
container(
self.answer_bar()
).width(Fill).align_x(Center).height(30)
]
.height(Fill)
.width(Fill)
]
.align_x(Left)
.width(Fill),
)
.center_x(Fill)
.padding(10)
.into()
}
fn draw_forward(&self) -> Element<'_, RepetitionMessage> {
let word = &self.current_word;
match self.settings.forward.as_str() {
"key" => self.draw_key(word),
"value" => self.draw_value(word),
_ => space().into(),
}
}
fn draw_backward(&self) -> Element<'_, RepetitionMessage> {
if !self.open {
return space().into();
}
let word = &self.current_word;
match self.settings.backward.as_str() {
"key" => self.draw_key(word),
"value" => self.draw_value(word),
_ => space().into(),
}
}
fn answer_bar(&self) -> Element<'_, RepetitionMessage> {
if !self.open {
return space().into();
}
row![
button("Не получилось").on_press(RepetitionMessage::Answer(WordOpenMode::None)),
button("Трудно").on_press(RepetitionMessage::Answer(WordOpenMode::Hard)),
button("Нормально").on_press(RepetitionMessage::Answer(WordOpenMode::Ok)),
button("Легко").on_press(RepetitionMessage::Answer(WordOpenMode::Easy)),
]
.spacing(10)
.into()
}
fn draw_key(&self, word: &DictionaryElement) -> Element<'_, RepetitionMessage> {
text!("{}", word.key).size(36).into()
}
fn draw_value(&self, word: &DictionaryElement) -> Element<'_, RepetitionMessage> {
text!("{}", word.value).size(24).into()
}
}
#[derive(Debug, Clone)]
impl KeyPressedPage for RepetitionState {
fn press(&mut self, message: &keyboard::Event) {
if let keyboard::Event::KeyPressed {
key: _,
modified_key: _,
physical_key: pk,
location: _,
modifiers: _,
text: _,
repeat: _,
} = message
{
if let Code(code) = pk {
match code {
keyboard::key::Code::Space => self.next(),
keyboard::key::Code::Digit1 => self.answer(WordOpenMode::None),
keyboard::key::Code::Digit2 => self.answer(WordOpenMode::Hard),
keyboard::key::Code::Digit3 => self.answer(WordOpenMode::Ok),
keyboard::key::Code::Digit4 => self.answer(WordOpenMode::Easy),
_ => {}
}
}
}
}
}
#[derive(Clone)]
pub enum RepetitionMessage {
Next,
Back,
SwitchShowMode(bool),
Answer(WordOpenMode),
}
+6 -1
View File
@@ -24,7 +24,11 @@ impl NavigatedPage<RepetitionsMessage> for RepetitionsState {
}
else if let RepetitionsMessage::GoToRepetition = message {
let clone = self.state.clone();
Some(Repetition(RepetitionState::new(self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].clone(), clone) ))
let card_set;
{
card_set = self.state.lock().unwrap().card_sets[self.selected_set.unwrap()].clone();
}
Some(Repetition(RepetitionState::new(card_set, clone) ))
} else {
None
}
@@ -212,6 +216,7 @@ impl RepetitionsState {
column
}
}
#[derive(Debug, Clone)]
pub enum RepetitionsMessage {
Next,