2026-08-23 22:16:02 +03:00
|
|
|
use crate::app::{fill_state, AppState};
|
2026-08-04 16:53:09 +03:00
|
|
|
use crate::data_provider::history::{get_history_of_set, history_dir};
|
2026-08-13 09:06:22 +03:00
|
|
|
use crate::data_provider::sqlite::{create_db, default_connection};
|
2026-08-20 18:18:14 +03:00
|
|
|
use crate::data_provider::web_api::{get_local_version, get_web_version, load_data, set_local_version};
|
2026-08-23 21:00:48 +03:00
|
|
|
use crate::dictionary::{app_data_dir, DictionaryMessage, DictionaryState};
|
2026-08-20 17:48:03 +03:00
|
|
|
use crate::dictionary_quiz::{DictionaryQuizMessage, DictionaryQuizState};
|
2026-08-01 13:04:18 +03:00
|
|
|
use crate::history::{HistoryMessage, HistoryState};
|
2026-08-15 13:22:59 +03:00
|
|
|
use crate::import::{ImportMessage, ImportState};
|
2026-08-18 17:31:19 +03:00
|
|
|
use crate::lang::Id;
|
2026-08-01 19:43:07 +03:00
|
|
|
use crate::message_navigation;
|
2026-08-01 13:04:18 +03:00
|
|
|
use crate::navigation::Page::*;
|
2026-08-23 22:16:02 +03:00
|
|
|
use crate::navigation::RootMessage::{DataLoaded, Keyboard, Notification, PushNotification, UpdateData};
|
|
|
|
|
use crate::notification::{NotificationData, NotificationManager, NotificationMessage, NotificationRequest};
|
2026-08-01 13:04:18 +03:00
|
|
|
use crate::quiz::{QuizMessage, QuizState};
|
2026-08-01 19:43:07 +03:00
|
|
|
use crate::randomizer::{RandomizerMessage, RandomizerState};
|
2026-08-01 13:04:18 +03:00
|
|
|
use crate::repetition::{RepetitionMessage, RepetitionState};
|
2026-08-07 00:00:57 +03:00
|
|
|
use crate::repetition_settings::{RepetitionSettingsMessage, RepetitionSettingsState};
|
2026-08-01 13:04:18 +03:00
|
|
|
use crate::repetitions::{RepetitionsMessage, RepetitionsState};
|
|
|
|
|
use crate::selector::{SelectorMessage, SelectorState};
|
2026-08-13 09:06:22 +03:00
|
|
|
use crate::state_navigate_handle;
|
2026-08-01 19:43:07 +03:00
|
|
|
use crate::state_update;
|
2026-08-01 13:04:18 +03:00
|
|
|
use crate::sync::{SyncMessage, SyncState};
|
2026-08-01 19:43:07 +03:00
|
|
|
use crate::view_navigation;
|
2026-08-01 13:04:18 +03:00
|
|
|
use crate::word::{WordMessage, WordState};
|
|
|
|
|
use crate::writing::{WritingMessage, WritingState};
|
2026-08-04 16:53:09 +03:00
|
|
|
use chrono::NaiveDate;
|
2026-08-18 17:31:19 +03:00
|
|
|
use hashbrown::HashMap;
|
2026-08-01 19:43:07 +03:00
|
|
|
use iced::keyboard::Event;
|
2026-08-23 21:00:48 +03:00
|
|
|
use iced::widget::stack;
|
2026-08-01 19:43:07 +03:00
|
|
|
use iced::{Element, Task};
|
2026-08-23 22:16:02 +03:00
|
|
|
use iced_core::Length::Fill;
|
2026-08-20 00:49:49 +03:00
|
|
|
use parking_lot::Mutex;
|
2026-08-18 17:31:19 +03:00
|
|
|
use rusqlite::Connection;
|
2026-08-20 00:07:09 +03:00
|
|
|
use std::sync::Arc;
|
2026-08-09 13:13:15 +03:00
|
|
|
use std::time::Instant;
|
2026-08-23 22:48:00 +03:00
|
|
|
use log::info;
|
2026-08-01 13:04:18 +03:00
|
|
|
|
|
|
|
|
impl Default for ScreenState {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
let mut state = AppState::new();
|
|
|
|
|
fill_state(&mut state);
|
|
|
|
|
let state = Arc::new(Mutex::new(state));
|
|
|
|
|
ScreenState {
|
2026-08-04 16:53:09 +03:00
|
|
|
stack: vec![Selector(SelectorState::new(state.clone()))],
|
|
|
|
|
app_state: state.clone(),
|
2026-08-23 21:00:48 +03:00
|
|
|
notification_manager: NotificationManager::new(),
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub enum RootMessage {
|
|
|
|
|
Selector(SelectorMessage),
|
|
|
|
|
Quiz(QuizMessage),
|
|
|
|
|
Writing(WritingMessage),
|
|
|
|
|
Dictionary(DictionaryMessage),
|
|
|
|
|
DictionaryQuiz(DictionaryQuizMessage),
|
|
|
|
|
Randomizer(RandomizerMessage),
|
|
|
|
|
Repetitions(RepetitionsMessage),
|
|
|
|
|
Repetition(RepetitionMessage),
|
|
|
|
|
Word(WordMessage),
|
|
|
|
|
Sync(SyncMessage),
|
|
|
|
|
History(HistoryMessage),
|
2026-08-04 16:53:09 +03:00
|
|
|
RepetitionSettings(RepetitionSettingsMessage),
|
2026-08-13 22:38:48 +03:00
|
|
|
Import(ImportMessage),
|
2026-08-01 13:04:18 +03:00
|
|
|
Keyboard(Event),
|
2026-08-18 12:47:06 +03:00
|
|
|
DataLoaded(HashMap<Id, Vec<(NaiveDate, u32)>>),
|
2026-08-23 21:00:48 +03:00
|
|
|
PushNotification(NotificationRequest),
|
|
|
|
|
Notification(NotificationMessage),
|
2026-08-13 09:06:22 +03:00
|
|
|
None,
|
|
|
|
|
UpdateData,
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub enum Page {
|
|
|
|
|
Selector(SelectorState),
|
|
|
|
|
Quiz(QuizState),
|
|
|
|
|
Writing(WritingState),
|
|
|
|
|
Dictionary(DictionaryState),
|
|
|
|
|
DictionaryQuiz(DictionaryQuizState),
|
|
|
|
|
Randomizer(RandomizerState),
|
|
|
|
|
Repetitions(RepetitionsState),
|
|
|
|
|
Repetition(RepetitionState),
|
|
|
|
|
Word(WordState),
|
|
|
|
|
Sync(SyncState),
|
|
|
|
|
History(HistoryState),
|
2026-08-04 16:53:09 +03:00
|
|
|
RepetitionSettings(RepetitionSettingsState),
|
2026-08-13 22:38:48 +03:00
|
|
|
Import(ImportState),
|
2026-08-01 13:04:18 +03:00
|
|
|
PreviousPage,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct ScreenState {
|
|
|
|
|
stack: Vec<Page>,
|
2026-08-04 16:53:09 +03:00
|
|
|
app_state: Arc<Mutex<AppState>>,
|
2026-08-23 21:00:48 +03:00
|
|
|
notification_manager: NotificationManager
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ScreenState {
|
|
|
|
|
pub fn boot() -> (ScreenState, Task<RootMessage>) {
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("Booting");
|
2026-08-01 13:04:18 +03:00
|
|
|
create_db();
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("Db created");
|
2026-08-13 09:06:22 +03:00
|
|
|
let state = ScreenState::default();
|
|
|
|
|
let reading_additional_task = Task::perform(Self::load_additional_data(), |data| data);
|
|
|
|
|
|
|
|
|
|
let final_task;
|
2026-08-20 00:07:09 +03:00
|
|
|
let state_guard = state.app_state.lock();
|
2026-08-13 09:06:22 +03:00
|
|
|
if state_guard.sync_data.auto_web_fetch {
|
|
|
|
|
let key = state_guard.sync_data.key.clone().unwrap();
|
|
|
|
|
final_task = Task::batch([
|
|
|
|
|
reading_additional_task,
|
2026-08-23 22:16:02 +03:00
|
|
|
Task::perform(
|
|
|
|
|
Self::load_web_backup(key),
|
|
|
|
|
|result| {
|
|
|
|
|
match result {
|
|
|
|
|
Ok(update) => {
|
|
|
|
|
if update {
|
|
|
|
|
UpdateData
|
|
|
|
|
}else {
|
|
|
|
|
RootMessage::None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => PushNotification(NotificationRequest::Warning(err))
|
|
|
|
|
}
|
2026-08-13 09:06:22 +03:00
|
|
|
}
|
2026-08-23 22:16:02 +03:00
|
|
|
),
|
2026-08-13 09:06:22 +03:00
|
|
|
]);
|
|
|
|
|
} else {
|
|
|
|
|
final_task = reading_additional_task;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drop(state_guard);
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("Boot finished");
|
2026-08-13 09:06:22 +03:00
|
|
|
(state, final_task)
|
2026-08-04 16:53:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn load_additional_data() -> RootMessage {
|
|
|
|
|
let directory = history_dir();
|
|
|
|
|
let mut map = HashMap::new();
|
2026-08-15 13:22:59 +03:00
|
|
|
for file in directory.read_dir().unwrap().flatten() {
|
|
|
|
|
let mut vec = vec![];
|
2026-08-19 19:21:21 +03:00
|
|
|
let history_file_name = match file.file_name().into_string() {
|
|
|
|
|
Err(_) => continue,
|
|
|
|
|
Ok(v) => v,
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-20 00:42:06 +03:00
|
|
|
let id = history_file_name[4..history_file_name.len() - 12]
|
2026-08-18 12:58:02 +03:00
|
|
|
.parse::<Id>()
|
|
|
|
|
.unwrap();
|
2026-08-04 16:53:09 +03:00
|
|
|
|
2026-08-15 13:22:59 +03:00
|
|
|
let history = get_history_of_set(id);
|
|
|
|
|
let by_date = history.chunk_by(|x, x1| {
|
|
|
|
|
x.timestamp.naive_local().date() == x1.timestamp.naive_local().date()
|
|
|
|
|
});
|
|
|
|
|
for group in by_date {
|
|
|
|
|
vec.push((group[0].timestamp.naive_local().date(), group.len() as u32));
|
2026-08-04 16:53:09 +03:00
|
|
|
}
|
2026-08-15 13:22:59 +03:00
|
|
|
map.insert(id, vec);
|
2026-08-04 16:53:09 +03:00
|
|
|
}
|
|
|
|
|
DataLoaded(map)
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
2026-08-04 16:53:09 +03:00
|
|
|
|
2026-08-23 22:16:02 +03:00
|
|
|
async fn load_web_backup(string: String) -> Result<bool, NotificationData> {
|
2026-08-13 09:06:22 +03:00
|
|
|
let local = get_local_version().await;
|
|
|
|
|
let web = get_web_version(string.as_str()).await?;
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("{} {}", local, web);
|
2026-08-13 09:06:22 +03:00
|
|
|
if web > local {
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("Web version is newer than local version");
|
2026-08-20 18:18:14 +03:00
|
|
|
load_data(string, true).await?;
|
2026-08-13 09:06:22 +03:00
|
|
|
set_local_version(web).await;
|
2026-08-15 13:22:59 +03:00
|
|
|
} else {
|
2026-08-13 09:06:22 +03:00
|
|
|
return Ok(false);
|
|
|
|
|
}
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 13:04:18 +03:00
|
|
|
pub fn update(&mut self, message: RootMessage) -> Task<RootMessage> {
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("Updating");
|
2026-08-09 13:13:15 +03:00
|
|
|
let time = Instant::now();
|
2026-08-01 13:04:18 +03:00
|
|
|
if let Keyboard(e) = message {
|
|
|
|
|
return self.handle_keyboard(e);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 16:53:09 +03:00
|
|
|
if let DataLoaded(data) = message {
|
2026-08-20 00:07:09 +03:00
|
|
|
self.app_state.lock().activity = data;
|
2026-08-04 16:53:09 +03:00
|
|
|
return Task::none();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-15 13:22:59 +03:00
|
|
|
if let UpdateData = message {
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("Loading data");
|
2026-08-20 00:07:09 +03:00
|
|
|
let mut state = self.app_state.lock();
|
2026-08-18 17:31:19 +03:00
|
|
|
if cfg!(windows) {
|
2026-08-16 23:50:14 +03:00
|
|
|
state.connection = Connection::open_in_memory().unwrap();
|
|
|
|
|
}
|
2026-08-13 09:06:22 +03:00
|
|
|
let path = app_data_dir();
|
|
|
|
|
let db_file = path.join("data.db");
|
|
|
|
|
let temp_db_file = path.join("data.db.tmp");
|
|
|
|
|
std::fs::rename(temp_db_file, &db_file).unwrap();
|
|
|
|
|
|
|
|
|
|
state.connection = default_connection();
|
|
|
|
|
fill_state(&mut state);
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
2026-08-23 21:00:48 +03:00
|
|
|
|
|
|
|
|
if let Notification(message) = message {
|
|
|
|
|
return self.notification_manager.update(message);
|
|
|
|
|
}
|
2026-08-23 22:16:02 +03:00
|
|
|
|
2026-08-23 21:00:48 +03:00
|
|
|
if let PushNotification(notification) = &message {
|
|
|
|
|
return self.notification_manager.push(notification.clone())
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 00:00:57 +03:00
|
|
|
let task: (Task<RootMessage>, bool) = self.navigate_or_update(message);
|
2026-08-04 16:53:09 +03:00
|
|
|
|
2026-08-07 00:00:57 +03:00
|
|
|
if task.1 {
|
|
|
|
|
state_navigate_handle!(
|
|
|
|
|
self.stack,
|
|
|
|
|
Selector,
|
|
|
|
|
Quiz,
|
|
|
|
|
Writing,
|
|
|
|
|
Dictionary,
|
|
|
|
|
DictionaryQuiz,
|
|
|
|
|
Randomizer,
|
|
|
|
|
Repetitions,
|
|
|
|
|
Repetition,
|
|
|
|
|
Word,
|
|
|
|
|
Sync,
|
|
|
|
|
History,
|
2026-08-13 22:38:48 +03:00
|
|
|
RepetitionSettings,
|
|
|
|
|
Import
|
2026-08-07 00:00:57 +03:00
|
|
|
);
|
|
|
|
|
}
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("Update took {:?}", time.elapsed());
|
2026-08-07 00:00:57 +03:00
|
|
|
|
|
|
|
|
task.0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn navigate_or_update(&mut self, message: RootMessage) -> (Task<RootMessage>, bool) {
|
|
|
|
|
state_update!(
|
2026-08-13 09:06:22 +03:00
|
|
|
message,
|
|
|
|
|
self.stack,
|
|
|
|
|
Selector,
|
|
|
|
|
Quiz,
|
|
|
|
|
Writing,
|
|
|
|
|
Dictionary,
|
|
|
|
|
DictionaryQuiz,
|
|
|
|
|
Randomizer,
|
|
|
|
|
Repetitions,
|
|
|
|
|
Repetition,
|
|
|
|
|
Word,
|
|
|
|
|
Sync,
|
|
|
|
|
History,
|
2026-08-13 22:38:48 +03:00
|
|
|
RepetitionSettings,
|
|
|
|
|
Import
|
2026-08-13 09:06:22 +03:00
|
|
|
)
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
2026-08-07 00:00:57 +03:00
|
|
|
|
2026-08-01 13:04:18 +03:00
|
|
|
pub fn view(&self) -> Element<'_, RootMessage> {
|
2026-08-23 21:00:48 +03:00
|
|
|
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("View update");
|
2026-08-09 13:13:15 +03:00
|
|
|
let time = Instant::now();
|
|
|
|
|
let view = view_navigation!(
|
2026-08-01 13:04:18 +03:00
|
|
|
self.stack,
|
|
|
|
|
Quiz,
|
|
|
|
|
Selector,
|
|
|
|
|
Writing,
|
|
|
|
|
Dictionary,
|
|
|
|
|
DictionaryQuiz,
|
|
|
|
|
Randomizer,
|
|
|
|
|
Repetitions,
|
|
|
|
|
Repetition,
|
|
|
|
|
Word,
|
|
|
|
|
Sync,
|
2026-08-04 16:53:09 +03:00
|
|
|
History,
|
2026-08-13 22:38:48 +03:00
|
|
|
RepetitionSettings,
|
|
|
|
|
Import
|
2026-08-09 13:13:15 +03:00
|
|
|
);
|
|
|
|
|
|
2026-08-23 21:00:48 +03:00
|
|
|
let notifications = self.notification_manager.draw().map(RootMessage::Notification);
|
|
|
|
|
|
2026-08-23 22:48:00 +03:00
|
|
|
info!("View took {:?}", time.elapsed());
|
2026-08-23 21:00:48 +03:00
|
|
|
stack![view, notifications].width(Fill).height(Fill).into()
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn handle_keyboard(&mut self, message: Event) -> Task<RootMessage> {
|
|
|
|
|
let page = self.stack.last_mut().unwrap();
|
|
|
|
|
match page {
|
2026-08-04 16:53:09 +03:00
|
|
|
Repetition(page) => page.press(&message),
|
2026-08-01 13:04:18 +03:00
|
|
|
_ => Task::none(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct AppSettings {
|
|
|
|
|
pub(crate) key: Option<String>,
|
2026-08-13 09:06:22 +03:00
|
|
|
pub(crate) auto_web_fetch: bool,
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[macro_export]
|
|
|
|
|
macro_rules! view_navigation {
|
|
|
|
|
($stack:expr, $($e:ident), *) => {
|
|
|
|
|
match &$stack.last().unwrap() {
|
|
|
|
|
$(
|
|
|
|
|
$e(s) => s.view().map(RootMessage::$e),
|
|
|
|
|
)*
|
|
|
|
|
_ => iced::widget::space().into(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[macro_export]
|
|
|
|
|
macro_rules! state_update {
|
|
|
|
|
($message:expr, $stack:expr, $($e:ident), *) => {
|
|
|
|
|
match $message {
|
|
|
|
|
$(
|
|
|
|
|
RootMessage::$e(msg) => {
|
|
|
|
|
if let $e(s) = $stack.last_mut().unwrap() {
|
|
|
|
|
message_navigation!(msg, $stack, s)
|
|
|
|
|
}
|
2026-08-07 00:00:57 +03:00
|
|
|
else {
|
|
|
|
|
return (Task::<RootMessage>::none(), false);
|
|
|
|
|
}
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
|
|
|
|
)*
|
2026-08-07 00:00:57 +03:00
|
|
|
_ => return (Task::<RootMessage>::none(), false),
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[macro_export]
|
|
|
|
|
macro_rules! message_navigation {
|
|
|
|
|
($msg:expr, $stack:expr, $state:expr) => {
|
|
|
|
|
if let Some(new_page) = $state.navigate(&$msg) {
|
|
|
|
|
if let Page::PreviousPage = new_page {
|
2026-08-19 19:21:21 +03:00
|
|
|
if $stack.len() > 1 {
|
|
|
|
|
$stack.pop();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-07 00:00:57 +03:00
|
|
|
return (Task::<RootMessage>::none(), true);
|
|
|
|
|
} else {
|
|
|
|
|
$stack.push(new_page);
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
2026-08-07 00:00:57 +03:00
|
|
|
|
|
|
|
|
return (Task::<RootMessage>::none(), false);
|
2026-08-01 13:04:18 +03:00
|
|
|
} else {
|
2026-08-07 00:00:57 +03:00
|
|
|
return ($state.update($msg), false);
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-08-07 00:00:57 +03:00
|
|
|
#[macro_export]
|
|
|
|
|
macro_rules! state_navigate_handle {
|
|
|
|
|
($stack:expr, $($e:ident), *) => {
|
|
|
|
|
match $stack.last_mut().unwrap() {
|
|
|
|
|
$(
|
|
|
|
|
Page::$e(page) => {
|
|
|
|
|
page.navigated();
|
|
|
|
|
}
|
|
|
|
|
)*
|
|
|
|
|
_ => {},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-01 13:04:18 +03:00
|
|
|
pub trait NavigatedPage<T> {
|
2026-08-18 12:14:35 +03:00
|
|
|
fn navigate(&mut self, message: &T) -> Option<Page>;
|
2026-08-06 23:20:14 +03:00
|
|
|
fn navigated(&mut self);
|
2026-08-13 22:03:47 +03:00
|
|
|
fn update(&mut self, message: T) -> Task<RootMessage>;
|
|
|
|
|
fn view(&self) -> Element<'_, T>;
|
2026-08-01 13:04:18 +03:00
|
|
|
}
|
|
|
|
|
pub trait KeyPressedPage {
|
|
|
|
|
fn press(&mut self, message: &Event) -> Task<RootMessage>;
|
|
|
|
|
}
|