repetition works
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user