From 9cb034066e1ba6e39568ce87779fc44ee0bf167e Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 5 May 2026 12:02:59 +0300 Subject: [PATCH] Add word group to database and model --- src/data_provider/words.rs | 135 +++++++++++++++++++++---------------- src/dictionary.rs | 7 +- src/dictionary_test.rs | 8 +-- src/lang.rs | 17 +++-- src/main.rs | 39 ++++++----- src/repetition.rs | 109 +++++++++++++++++++----------- src/repetitions.rs | 4 +- src/word.rs | 6 +- 8 files changed, 193 insertions(+), 132 deletions(-) diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 1f0cdf9..b81d89e 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,5 +1,5 @@ use crate::dictionary::app_data_dir; -use crate::lang::DictionaryElement; +use crate::lang::{WordData, WordGroup}; use rusqlite::Connection; use std::collections::HashMap; @@ -10,13 +10,61 @@ pub fn create_db() { std::fs::File::create(&db_file).unwrap(); let connection = Connection::open(&db_file).unwrap(); connection.execute("PRAGMA foreign_keys = ON;", []).unwrap(); - create_words_table(&connection); - create_card_stat_table(&connection); - create_card_set_table(&connection); + + create_tables(&connection); } } -pub fn add_word(word: &mut DictionaryElement, connection: &Connection) { +fn create_tables(conn: &Connection) { + conn.execute( + " + CREATE TABLE word_group ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL + ); + + insert into word_group (name) + values (\"Слова\"); + + CREATE TABLE words ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL, + value TEXT NOT NULL, + tags TEXT NOT NULL, + more TEXT, + group_id INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY(group_id) REFERENCES word_group(id) + ); + + CREATE TABLE card_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + word_id INTEGER NOT NULL, + set_id TEXT NOT NULL, + score INTEGER NOT NULL DEFAULT 1, + last_opened INTEGER NOT NULL, + FOREIGN KEY (word_id) REFERENCES words (id) ON DELETE CASCADE, + FOREIGN KEY (set_id) REFERENCES card_set (id) ON DELETE CASCADE + ); + + + + CREATE TABLE card_set ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + forward TEXT NOT NULL, + backward TEXT NOT NULL, + filter TEXT NOT NULL + ); + ", + (), + ) + .unwrap_or_else(|e| { + println!("{}", e); + 0 + }); +} + +pub fn add_word(word: &mut WordData, connection: &Connection) { let index = connection .query_row( "INSERT INTO words (key, value, tags, more) VALUES (?1, ?2, ?3, ?4) RETURNING id", @@ -36,7 +84,7 @@ pub fn add_word(word: &mut DictionaryElement, connection: &Connection) { word.id = index; } -pub fn update_word(word: &mut DictionaryElement, connection: &Connection) { +pub fn update_word(word: &mut WordData, connection: &Connection) { if word.id == 0 { add_word(word, &connection); } else { @@ -58,7 +106,7 @@ pub fn update_word(word: &mut DictionaryElement, connection: &Connection) { } } -pub fn delete_word(word: &DictionaryElement, connection: &Connection) { +pub fn delete_word(word: &WordData, connection: &Connection) { if word.id == 0 { return; } @@ -70,19 +118,20 @@ pub fn delete_word(word: &DictionaryElement, connection: &Connection) { }); } -pub fn load_words(connection: &Connection) -> Vec { +pub fn load_words(connection: &Connection) -> Vec { let mut stmt = connection - .prepare("SELECT id, key, value, tags, more FROM words") + .prepare("SELECT id, key, value, tags, more, group_id FROM words") .unwrap(); let word_iter = stmt .query_map([], |row| { let addinionals: String = row.get(4)?; - Ok(DictionaryElement { + Ok(WordData { id: row.get(0)?, key: row.get(1)?, value: row.get(2)?, tags: row.get(3)?, additional: serde_json::from_str::>(&addinionals).unwrap(), + group_id: row.get(5)?, }) }) .unwrap(); @@ -95,55 +144,23 @@ pub fn load_words(connection: &Connection) -> Vec { buffer } -fn create_words_table(conn: &Connection) { - conn.execute( - "CREATE TABLE words ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key TEXT NOT NULL, - value TEXT NOT NULL, - tags TEXT NOT NULL, - more TEXT - )", - (), - ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); -} +pub fn load_word_groups(connection: &Connection) -> Vec { + let mut stmt = connection + .prepare("SELECT id, name FROM word_group") + .unwrap(); + let group_iter = stmt + .query_map([], |row| { + Ok(WordGroup { + id: row.get(0)?, + name: row.get(1)?, + }) + }) + .unwrap(); -fn create_card_stat_table(conn: &Connection) { - conn.execute( - "CREATE TABLE card_stats ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - word_id INTEGER NOT NULL, - set_id TEXT NOT NULL, - score INTEGER NOT NULL DEFAULT 1, - last_opened INTEGER NOT NULL, - FOREIGN KEY (word_id) REFERENCES words (id) ON DELETE CASCADE, - FOREIGN KEY (set_id) REFERENCES card_set (id) ON DELETE CASCADE - )", - (), - ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); -} + let mut buffer = vec![]; + for group in group_iter { + buffer.push(group.unwrap()); + } -fn create_card_set_table(conn: &Connection) { - conn.execute( - "CREATE TABLE card_set ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - forward TEXT NOT NULL, - backward TEXT NOT NULL, - filter TEXT NOT NULL - )", - (), - ) - .unwrap_or_else(|e| { - println!("{}", e); - 0 - }); + buffer } \ No newline at end of file diff --git a/src/dictionary.rs b/src/dictionary.rs index d2e29bb..74d98bd 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -1,7 +1,7 @@ use crate::data_provider::words::{delete_word, update_word}; use crate::dictionary::DictionaryMessage::Test; use crate::dictionary_test::DictionaryQuizState; -use crate::lang::DictionaryElement; +use crate::lang::WordData; use crate::word::WordState; use crate::Page::Word; use crate::{AppState, NavigatedPage, Page, RootMessage, DEFAULT_SPACING}; @@ -41,6 +41,7 @@ pub enum DictionaryMessage { SetReverse(bool), Search(String), SetTyping(bool), + } impl NavigatedPage for DictionaryState { @@ -66,7 +67,7 @@ impl NavigatedPage for DictionaryState { } } if let DictionaryMessage::WordAction(index) = message { - let word : DictionaryElement; + let word : WordData; { let state = self.state.lock().unwrap(); let dict = &state.dictionary; @@ -101,7 +102,7 @@ impl DictionaryState { match message { DictionaryMessage::NewWord => { let dict = &mut self.state.lock().unwrap().dictionary; - dict.push(DictionaryElement::new()); + dict.push(WordData::new()); self.include_map.push(false); } diff --git a/src/dictionary_test.rs b/src/dictionary_test.rs index 0f266a5..cae6b05 100644 --- a/src/dictionary_test.rs +++ b/src/dictionary_test.rs @@ -9,12 +9,12 @@ use iced::widget::{button, container, row, space, text, text_input, Row}; use iced::Background::Color; use iced::{alignment, Border, Element, Fill, Task, Theme}; use rand::prelude::SliceRandom; -use crate::lang::DictionaryElement; +use crate::lang::WordData; #[derive(Debug, Clone)] pub struct DictionaryQuizState { - words: Vec, - current_set: Vec, + words: Vec, + current_set: Vec, answer: String, view: String, correct: String, @@ -43,7 +43,7 @@ impl NavigatedPage for DictionaryQuizState { impl DictionaryQuizState { pub fn new( - words: Vec, + words: Vec, reverse: bool, no_typing: bool, ) -> DictionaryQuizState { diff --git a/src/lang.rs b/src/lang.rs index 4f832f1..e0ad0de 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -229,15 +229,16 @@ impl PartialEq for KanaSet { } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DictionaryElement { +pub struct WordData { pub id: u32, pub key: String, pub value: String, pub tags: String, pub additional: HashMap, + pub group_id: u32 } -impl DictionaryElement { +impl WordData { pub fn new() -> Self { Self { id: 0, @@ -245,10 +246,16 @@ impl DictionaryElement { value: String::new(), tags: String::new(), additional: Default::default(), + group_id: 1 } } } +pub struct WordGroup{ + pub id: u32, + pub name: String, +} + #[derive(Clone, PartialEq)] pub struct CardStatistics { pub id: u32, @@ -302,7 +309,7 @@ pub enum WordOpenMode { #[derive(Clone)] pub struct CardSet { - words: Vec, + words: Vec, set: Vec, last_weights: WeightedIndex, current_word_index: Option, @@ -360,7 +367,7 @@ impl CardSet { } - pub fn next(&mut self) -> DictionaryElement { + pub fn next(&mut self) -> (WordData, CardStatistics) { let index = self.last_weights.sample(&mut self.generator); if self.history.contains(&index) { @@ -372,7 +379,7 @@ impl CardSet { } self.history.push(index); self.current_word_index = Some(index); - self.words[index].clone() + (self.words[index].clone(), self.set[index].clone()) } pub fn open(&mut self, status: WordOpenMode) { diff --git a/src/main.rs b/src/main.rs index eac7841..bc415a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ #![windows_subsystem = "windows"] +mod data_provider; mod dictionary; mod dictionary_test; mod lang; @@ -7,15 +8,14 @@ mod randomizer; mod repetition; mod repetitions; mod selector; -mod writing; -mod data_provider; mod word; +mod writing; use crate::data_provider::card_sets::load_sets; -use crate::data_provider::words::{create_db, load_words}; +use crate::data_provider::words::{create_db, load_word_groups, load_words}; use crate::dictionary::{app_data_dir, DictionaryMessage, DictionaryState}; use crate::dictionary_test::{DictionaryQuizMessage, DictionaryQuizState}; -use crate::lang::DictionaryElement; +use crate::lang::{WordData, WordGroup}; use crate::quiz::*; use crate::randomizer::randomizer::{RandomizerMessage, RandomizerState}; use crate::repetition::{RepetitionMessage, RepetitionState}; @@ -24,7 +24,7 @@ use crate::selector::*; use crate::word::{WordMessage, WordState}; use crate::writing::{WritingMessage, WritingState}; use crate::Page::{ - Dictionary, DictionaryQuiz, Quiz, Randomizer, Repetition, Repetitions, Selector, Word, Writing + Dictionary, DictionaryQuiz, Quiz, Randomizer, Repetition, Repetitions, Selector, Word, Writing, }; use crate::RootMessage::Keyboard; use iced::keyboard::Event; @@ -37,12 +37,12 @@ use std::sync::{Arc, Mutex}; const DEFAULT_SPACING: f32 = 10.0; fn main() -> iced::Result { - iced::application(ScreenState::boot, ScreenState::update, ScreenState::view).subscription(subscription) + 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")) - . - run() + .run() } fn subscription(_state: &ScreenState) -> Subscription { @@ -81,9 +81,10 @@ pub struct ScreenState { } pub struct AppState { - pub dictionary: Vec, + pub dictionary: Vec, pub card_sets: Vec, - pub connection: Connection + pub word_groups: Vec, + pub connection: Connection, } impl Default for ScreenState { @@ -92,10 +93,16 @@ impl Default for ScreenState { let db_file = path.join("data.db"); let connection = Connection::open(db_file).unwrap(); connection.execute("PRAGMA foreign_keys = ON;", []).unwrap(); - let list: Vec = load_words(&connection); - let sets: Vec = load_sets(&connection); + let list = load_words(&connection); + let sets = load_sets(&connection); + let groups = load_word_groups(&connection); - let state = Arc::new(Mutex::new(AppState { dictionary: list, card_sets: sets, connection })); + let state = Arc::new(Mutex::new(AppState { + dictionary: list, + card_sets: sets, + connection, + word_groups: groups, + })); ScreenState { stack: vec![Selector(SelectorState::new(state.clone()))], } @@ -147,12 +154,10 @@ impl ScreenState { return match page { Repetition(page) => page.press(&message), _ => Task::none(), - } + }; } } - - #[macro_export] macro_rules! view_navigation { ($stack:expr, $($e:ident), *) => { @@ -212,4 +217,4 @@ pub trait KeyPressedPage { shadow: Default::default(), snap: false, } -}*/ \ No newline at end of file +}*/ diff --git a/src/repetition.rs b/src/repetition.rs index e82b59a..871b4a3 100644 --- a/src/repetition.rs +++ b/src/repetition.rs @@ -1,14 +1,14 @@ -use std::collections::HashSet; use crate::data_provider::voice::get_voice; -use crate::lang::{CardSet, DictionaryElement, WordOpenMode}; +use crate::lang::{CardSet, CardStatistics, WordData, WordOpenMode}; use crate::repetitions::CardSetSettings; use crate::Page::PreviousPage; use crate::{AppState, KeyPressedPage, NavigatedPage, Page, RootMessage, DEFAULT_SPACING}; use iced::alignment::Horizontal::Center; use iced::keyboard::key::Physical::Code; -use iced::widget::{button, column, container, row, rule, space, text}; +use iced::widget::{button, column, container, row, rule, space, text, Column}; use iced::{alignment, keyboard, Element, Fill, Left, Task}; use rodio::MixerDeviceSink; +use std::collections::HashSet; use std::sync::{Arc, Mutex}; use tokio::task::spawn_blocking; @@ -16,7 +16,8 @@ pub struct RepetitionState { pub settings: CardSetSettings, pub set: CardSet, pub state: Arc>, - current_word: DictionaryElement, + current_word: WordData, + current_statistic: CardStatistics, open: bool, can_play: bool, sink: Arc, @@ -36,7 +37,7 @@ impl NavigatedPage for RepetitionState { impl RepetitionState { pub(crate) fn new(set: CardSetSettings, state: Arc>) -> RepetitionState { let mut card_set = CardSet::new(&set, state.clone()); - let word = card_set.next(); + let (word, stat) = card_set.next(); let sink_handle = rodio::DeviceSinkBuilder::open_default_sink().unwrap(); RepetitionState { @@ -44,6 +45,7 @@ impl RepetitionState { set: card_set, state, current_word: word, + current_statistic: stat, open: false, can_play: true, sink: Arc::new(sink_handle), @@ -60,15 +62,17 @@ impl RepetitionState { RepetitionMessage::Answer(m) => return self.answer(m), RepetitionMessage::Play => { if !self.can_play { - return Task::none() + return Task::none(); } self.can_play = false; let value = self.current_word.key.clone(); if self.settings.require_speech() { - return Task::perform(play_sound(self.sink.clone(), value), |_| RootMessage::Repetition(RepetitionMessage::PlayFinished)); + return Task::perform(play_sound(self.sink.clone(), value), |_| { + RootMessage::Repetition(RepetitionMessage::PlayFinished) + }); } - }, + } RepetitionMessage::PlayFinished => { self.can_play = true; } @@ -77,8 +81,6 @@ impl RepetitionState { Task::none() } - - fn next(&mut self) -> Task { if self.open { self.answer(WordOpenMode::None) @@ -96,10 +98,15 @@ impl RepetitionState { self.set.open(mode); self.open = false; self.opened.insert(self.current_word.id); - self.current_word = self.set.next(); + let next = self.set.next(); + self.current_word = next.0; + self.current_statistic = next.1; if self.settings.require_speech() { - return Task::perform(play_sound(self.sink.clone(), self.current_word.key.clone()), |_| RootMessage::Repetition(RepetitionMessage::PlayFinished)); + return Task::perform( + play_sound(self.sink.clone(), self.current_word.key.clone()), + |_| RootMessage::Repetition(RepetitionMessage::PlayFinished), + ); } Task::none() } @@ -123,8 +130,13 @@ impl RepetitionState { container(self.answer_bar()) .width(Fill) .align_x(Center) - .height(30), - text!("Затронуто слов {}, {}%", self.opened.len(), (self.opened.len() as f32 / self.set.len() as f32 * 10000.0).round() / 100.0) + .height(60), + text!( + "Затронуто слов {}, {}%", + self.opened.len(), + (self.opened.len() as f32 / self.set.len() as f32 * 10000.0).round() + / 100.0 + ) ] .height(Fill) .width(Fill) @@ -138,13 +150,8 @@ impl RepetitionState { } 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), - "speech" => self.draw_voice(), - _ => space().into(), - } + self.draw_card_view(self.settings.forward.as_str()) + } fn draw_backward(&self) -> Element<'_, RepetitionMessage> { @@ -152,13 +159,26 @@ impl RepetitionState { return space().into(); } + + self.draw_card_view(self.settings.backward.as_str()) + } + + fn draw_card_view(&self, properties: &str) -> Element<'_, RepetitionMessage> { let word = &self.current_word; - match self.settings.backward.as_str() { - "key" => self.draw_key(word), - "value" => self.draw_value(word), - "speech" => self.draw_voice(), - _ => space().into(), + + let mut col = Column::new(); + + for view_type in properties.split(" ") { + col = col.push( match view_type { + "key" => self.draw_key(word), + "value" => self.draw_value(word), + "speech" => self.draw_voice(), + "reading" => self.draw_reading(word), + _ => space().into(), + }) } + + col.spacing(DEFAULT_SPACING).into() } fn answer_bar(&self) -> Element<'_, RepetitionMessage> { @@ -166,29 +186,40 @@ impl RepetitionState { 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)), + column![ + text!("{} очков", self.current_statistic.calculated_score().round() as i32), + 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(DEFAULT_SPACING) ] + .align_x(Center) .spacing(DEFAULT_SPACING) .into() } - fn draw_key(&self, word: &DictionaryElement) -> Element<'_, RepetitionMessage> { + fn draw_key(&self, word: &WordData) -> Element<'_, RepetitionMessage> { text!("{}", word.key).size(36).into() } - fn draw_value(&self, word: &DictionaryElement) -> Element<'_, RepetitionMessage> { + fn draw_value(&self, word: &WordData) -> Element<'_, RepetitionMessage> { text!("{}", word.value).size(24).into() } fn draw_voice(&self) -> Element<'_, RepetitionMessage> { - button("Воспроизвести") .on_press(RepetitionMessage::Play) .into() } + + fn draw_reading(&self, word: &WordData) -> Element<'_, RepetitionMessage> { + match word.additional.get("reading") { + None => space().into(), + Some(reading) => text!("{}", reading).size(24).into(), + } + } } impl KeyPressedPage for RepetitionState { @@ -211,12 +242,10 @@ impl KeyPressedPage for RepetitionState { keyboard::key::Code::Digit3 => self.answer(WordOpenMode::Ok), keyboard::key::Code::Digit4 => self.answer(WordOpenMode::Easy), _ => Task::none(), - } + }; } - } Task::none() - } } @@ -233,5 +262,7 @@ async fn play_sound(sink: Arc, text: String) { let data = get_voice(text.as_str()).await; spawn_blocking(move || { rodio::play(&sink.mixer(), data).unwrap().sleep_until_end(); - }).await.unwrap(); -} \ No newline at end of file + }) + .await + .unwrap(); +} diff --git a/src/repetitions.rs b/src/repetitions.rs index 1505b08..c1c833c 100644 --- a/src/repetitions.rs +++ b/src/repetitions.rs @@ -1,5 +1,5 @@ use crate::data_provider::card_sets::{delete_set, update_card_set}; -use crate::lang::DictionaryElement; +use crate::lang::WordData; use crate::repetition::RepetitionState; use crate::Page::{PreviousPage, Repetition}; use crate::{AppState, NavigatedPage, Page, RootMessage, DEFAULT_SPACING}; @@ -255,7 +255,7 @@ impl CardSetSettings { ast.is_ok() } - pub fn get_word_list(&self, state: &AppState) -> Vec { + pub fn get_word_list(&self, state: &AppState) -> Vec { let mut list = vec![]; let engine = Engine::new(); let ast = engine.compile(&self.filter); diff --git a/src/word.rs b/src/word.rs index 9d12614..e1b294f 100644 --- a/src/word.rs +++ b/src/word.rs @@ -1,5 +1,5 @@ use crate::data_provider::words::{delete_word, update_word}; -use crate::lang::DictionaryElement; +use crate::lang::WordData; use crate::Page::PreviousPage; use crate::{AppState, NavigatedPage, Page, RootMessage, DEFAULT_SPACING}; use iced::widget::button::danger; @@ -11,7 +11,7 @@ use std::sync::{Arc, Mutex}; pub struct WordState { state: Arc>, index: usize, - word: DictionaryElement, + word: WordData, } impl NavigatedPage for WordState { @@ -26,7 +26,7 @@ impl NavigatedPage for WordState { impl WordState { pub(crate) fn new( - word: DictionaryElement, + word: WordData, index: usize, state: Arc>, ) -> WordState {