More notifications and move start code to another file
This commit is contained in:
1 parent
e1bfd9494d
commit
5a926b1419
17 files changed
+239
-197
No files matched your search
+109
@@ -0,0 +1,109 @@
|
|||||||
|
use chrono::NaiveDate;
|
||||||
|
use hashbrown::HashMap;
|
||||||
|
use iced::{keyboard, window, Subscription};
|
||||||
|
use iced_core::{Font, Size, Theme};
|
||||||
|
use iced_core::window::Position;
|
||||||
|
use iced_core::window::settings::PlatformSpecific;
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use crate::data_provider::card_sets::load_sets;
|
||||||
|
use crate::data_provider::settings::get_setting;
|
||||||
|
use crate::data_provider::sqlite::default_connection;
|
||||||
|
use crate::data_provider::words::{load_word_groups, load_words};
|
||||||
|
use crate::lang::{DeckSettings, Id, WordData, WordGroup};
|
||||||
|
use crate::navigation::{AppSettings, RootMessage, ScreenState};
|
||||||
|
use crate::navigation::RootMessage::Keyboard;
|
||||||
|
pub const USER_FONT: Font = Font::with_name("Noto Sans JP");
|
||||||
|
|
||||||
|
pub fn start() -> iced::Result {
|
||||||
|
println!("Welcome to JapLearn");
|
||||||
|
iced::application(ScreenState::boot, ScreenState::update, ScreenState::view)
|
||||||
|
.window(window_settings())
|
||||||
|
.subscription(subscription)
|
||||||
|
.title("JapLearn")
|
||||||
|
.settings(iced::Settings {
|
||||||
|
default_text_size: iced::Pixels(18.0),
|
||||||
|
..iced::Settings::default()
|
||||||
|
})
|
||||||
|
.font(include_bytes!("../noto.ttf"))
|
||||||
|
.default_font(USER_FONT)
|
||||||
|
.theme(Theme::GruvboxDark)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
fn window_settings() -> window::Settings {
|
||||||
|
let mut settings = window::Settings {
|
||||||
|
position: Position::Centered,
|
||||||
|
min_size: Some(Size::new(700.0_f32, 700.0_f32)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
settings.platform_specific = PlatformSpecific {
|
||||||
|
application_id: "JapLearn".to_string(),
|
||||||
|
override_redirect: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(_state: &ScreenState) -> Subscription<RootMessage> {
|
||||||
|
keyboard::listen().map(Keyboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub dictionary: Vec<WordData>,
|
||||||
|
pub decks: Vec<DeckSettings>,
|
||||||
|
pub word_groups: Vec<WordGroup>,
|
||||||
|
pub connection: Connection,
|
||||||
|
pub sync_data: AppSettings,
|
||||||
|
pub activity: HashMap<Id, Vec<(NaiveDate, u32)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
dictionary: vec![],
|
||||||
|
decks: vec![],
|
||||||
|
word_groups: vec![],
|
||||||
|
connection: default_connection(),
|
||||||
|
sync_data: AppSettings {
|
||||||
|
key: None,
|
||||||
|
auto_web_fetch: false,
|
||||||
|
},
|
||||||
|
activity: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_state(state: &mut AppState) {
|
||||||
|
let list = load_words(&state.connection);
|
||||||
|
let sets = load_sets(&state.connection);
|
||||||
|
let groups = load_word_groups(&state.connection);
|
||||||
|
let setting = load_settings(&state.connection);
|
||||||
|
|
||||||
|
state.dictionary = list;
|
||||||
|
state.decks = sets;
|
||||||
|
state.word_groups = groups;
|
||||||
|
state.sync_data = setting
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_settings(connection: &Connection) -> AppSettings {
|
||||||
|
let key = get_setting("SYNC_KEY".to_string(), connection);
|
||||||
|
let fetch = get_setting("AUTO_WEB_FETCH".to_string(), connection)
|
||||||
|
.unwrap_or("false".to_string())
|
||||||
|
.parse::<bool>()
|
||||||
|
.unwrap();
|
||||||
|
AppSettings {
|
||||||
|
key,
|
||||||
|
auto_web_fetch: fetch,
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
-41
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::notification::NotificationData;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -21,58 +22,73 @@ pub struct ImportNote {
|
|||||||
pub fields: Vec<String>,
|
pub fields: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_groups(connection: &Connection) -> ImportData {
|
pub fn load_groups(connection: &Connection) -> Result<ImportData, NotificationData> {
|
||||||
let json_models: String = connection
|
let json_models: String = connection
|
||||||
.query_one("select models from col", (), |row| row.get(0))
|
.query_one("select models from col", (), |row| row.get(0))
|
||||||
.unwrap();
|
.map_err(|_| NotificationData::IncorrectAnkiFile)?;
|
||||||
let raw: Value = serde_json::from_str(json_models.as_str()).unwrap();
|
let raw: Value = serde_json::from_str(json_models.as_str())
|
||||||
let sets = raw.as_object().unwrap();
|
.map_err(|_| NotificationData::IncorrectAnkiFile)?;
|
||||||
let mut total_data = ImportData(vec![]);
|
let sets = raw.as_object();
|
||||||
|
if let Some(sets) = sets {
|
||||||
|
let mut total_data = ImportData(vec![]);
|
||||||
|
|
||||||
let mut count_stmt = connection
|
let mut count_stmt = connection
|
||||||
.prepare("select mid, count(id) from notes group by mid")
|
.prepare("select mid, count(id) from notes group by mid")
|
||||||
.unwrap();
|
.map_err(|_| NotificationData::IncorrectAnkiFile)?;
|
||||||
|
|
||||||
let counts = count_stmt
|
let counts = count_stmt
|
||||||
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
|
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|x| x.unwrap())
|
.map(|x| x.unwrap())
|
||||||
.collect::<Vec<(i64, i64)>>();
|
.collect::<Vec<(i64, i64)>>();
|
||||||
|
|
||||||
for (_, collection) in sets {
|
for (_, collection) in sets {
|
||||||
let fields_property = collection.get("fields");
|
let fields_property = collection.get("fields");
|
||||||
if let Some(fields_property) = fields_property {
|
if let Some(fields_property) = fields_property {
|
||||||
let fields = fields_property
|
let fields = fields_property
|
||||||
.as_array()
|
.as_array()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x["name"].as_str().unwrap().to_string())
|
.filter_map(|x| x.get("name"))
|
||||||
.collect();
|
.map(|x| x.as_str().unwrap().to_string())
|
||||||
let mut group = ImportGroup {
|
.collect();
|
||||||
id: collection["id"].as_u64().unwrap(),
|
let id = collection.get("id");
|
||||||
name: collection["name"].as_str().unwrap().to_string(),
|
if id.is_none() {
|
||||||
fields,
|
return Err(NotificationData::IncorrectAnkiFile);
|
||||||
mapping: Default::default(),
|
}
|
||||||
length: 0,
|
let id = id.unwrap().as_u64().unwrap();
|
||||||
imported: false,
|
let name = collection.get("name");
|
||||||
};
|
if name.is_none() {
|
||||||
if let Some((_, count)) = counts.iter().find(|(id, _)| *id == group.id as i64) {
|
return Err(NotificationData::IncorrectAnkiFile);
|
||||||
group.length = *count as u64
|
}
|
||||||
|
let name = name.unwrap().as_str().unwrap().to_string();
|
||||||
|
|
||||||
|
let mut group = ImportGroup {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
fields,
|
||||||
|
mapping: Default::default(),
|
||||||
|
length: 0,
|
||||||
|
imported: false,
|
||||||
|
};
|
||||||
|
if let Some((_, count)) = counts.iter().find(|(id, _)| *id == group.id as i64) {
|
||||||
|
group.length = *count as u64
|
||||||
|
}
|
||||||
|
println!("{:?}", group);
|
||||||
|
total_data.0.push(group);
|
||||||
}
|
}
|
||||||
println!("{:?}", group);
|
|
||||||
total_data.0.push(group);
|
|
||||||
}
|
}
|
||||||
|
return Ok(total_data)
|
||||||
}
|
}
|
||||||
total_data
|
Err(NotificationData::IncorrectAnkiFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_words_of_group(connection: &Connection, group_id: u64) -> Vec<ImportNote> {
|
pub fn get_words_of_group(connection: &Connection, group_id: u64) -> Result<Vec<ImportNote>, NotificationData> {
|
||||||
let mut count_stmt = connection
|
let mut count_stmt = connection
|
||||||
.prepare("select tags, flds from notes where mid == ?1;")
|
.prepare("select tags, flds from notes where mid == ?1;")
|
||||||
.unwrap();
|
.map_err(|_| NotificationData::IncorrectAnkiFile)?;
|
||||||
|
|
||||||
count_stmt
|
Ok(count_stmt
|
||||||
.query_map((group_id as i64,), |row| {
|
.query_map((group_id as i64,), |row| {
|
||||||
Ok(ImportNote {
|
Ok(ImportNote {
|
||||||
tags: row.get(0)?,
|
tags: row.get(0)?,
|
||||||
@@ -83,7 +99,7 @@ pub fn get_words_of_group(connection: &Connection, group_id: u64) -> Vec<ImportN
|
|||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.map_err(|_| NotificationData::IncorrectAnkiFile)?
|
||||||
.map(|x| x.unwrap())
|
.map(|x| x.unwrap())
|
||||||
.collect::<Vec<ImportNote>>()
|
.collect::<Vec<ImportNote>>())
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,14 @@ use reqwest::Client;
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
|
use crate::notification::NotificationData;
|
||||||
|
use crate::notification::NotificationData::*;
|
||||||
|
|
||||||
pub async fn get_voice(text: &str) -> Result<BufReader<File>, ()> {
|
pub async fn get_voice(text: &str) -> Result<BufReader<File>, NotificationData> {
|
||||||
let mut path = app_data_dir();
|
let mut path = app_data_dir();
|
||||||
path.push("voice");
|
path.push("voice");
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
std::fs::create_dir(path.clone()).map_err(|_| ())?;
|
std::fs::create_dir(path.clone()).map_err(|_| PermissionDenied)?;
|
||||||
}
|
}
|
||||||
let hash = format!("{}.wav", hex::encode(Sha256::digest(text.as_bytes())));
|
let hash = format!("{}.wav", hex::encode(Sha256::digest(text.as_bytes())));
|
||||||
path.push(hash);
|
path.push(hash);
|
||||||
@@ -20,7 +22,7 @@ pub async fn get_voice(text: &str) -> Result<BufReader<File>, ()> {
|
|||||||
.send()
|
.send()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let query = query.map_err(|_| ())?.text().await.map_err(|_| ())?;
|
let query = query.map_err(|_| VoiceVoxUnavailable)?.text().await.map_err(|_| VoiceVoxUnavailable)?;
|
||||||
// Synthesis
|
// Synthesis
|
||||||
let audio = client
|
let audio = client
|
||||||
.post(format!("{engine_url}/synthesis?speaker=11"))
|
.post(format!("{engine_url}/synthesis?speaker=11"))
|
||||||
@@ -28,13 +30,13 @@ pub async fn get_voice(text: &str) -> Result<BufReader<File>, ()> {
|
|||||||
.body(query)
|
.body(query)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ())?
|
.map_err(|_| VoiceVoxUnavailable)?
|
||||||
.bytes()
|
.bytes()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ())?;
|
.map_err(|_| VoiceVoxUnavailable)?;
|
||||||
|
|
||||||
tokio::fs::write(&path, &audio).await.map_err(|_| ())?;
|
tokio::fs::write(&path, &audio).await.map_err(|_| PermissionDenied)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(BufReader::new(File::open(path).map_err(|_| ())?))
|
Ok(BufReader::new(File::open(path).map_err(|_| PermissionDenied)?))
|
||||||
}
|
}
|
||||||
+2
-2
@@ -4,10 +4,9 @@ use crate::dictionary_quiz::DictionaryQuizState;
|
|||||||
use crate::import::ImportState;
|
use crate::import::ImportState;
|
||||||
use crate::lang::{WordData, WordGroup};
|
use crate::lang::{WordData, WordGroup};
|
||||||
use crate::navigation::Page::{Import, Word};
|
use crate::navigation::Page::{Import, Word};
|
||||||
use crate::navigation::{NavigatedPage, Page};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
use crate::styling::*;
|
use crate::styling::*;
|
||||||
use crate::word::WordState;
|
use crate::word::WordState;
|
||||||
use crate::{AppState, RootMessage};
|
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use iced::alignment::Vertical::Center;
|
use iced::alignment::Vertical::Center;
|
||||||
@@ -25,6 +24,7 @@ use std::ops::Add;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DictionaryState {
|
pub struct DictionaryState {
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,7 @@
|
|||||||
use crate::data_provider::history::{get_history_of_set, HistoryItem};
|
use crate::data_provider::history::{get_history_of_set, HistoryItem};
|
||||||
use crate::lang::{WordData, INVALID_ID};
|
use crate::lang::{WordData, INVALID_ID};
|
||||||
use crate::navigation::{NavigatedPage, Page};
|
use crate::navigation::*;
|
||||||
use crate::styling::*;
|
use crate::styling::*;
|
||||||
use crate::{AppState, RootMessage};
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use iced::alignment::Horizontal::Center;
|
use iced::alignment::Horizontal::Center;
|
||||||
use iced::widget::space::horizontal;
|
use iced::widget::space::horizontal;
|
||||||
@@ -11,6 +10,7 @@ use iced::{Element, Fill, FillPortion, Task};
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use HistoryMessage::Back;
|
use HistoryMessage::Back;
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HistoryState {
|
pub struct HistoryState {
|
||||||
|
|||||||
+22
-23
@@ -1,10 +1,11 @@
|
|||||||
use crate::AppState;
|
|
||||||
use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups};
|
use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups};
|
||||||
use crate::data_provider::words::{add_group, add_words};
|
use crate::data_provider::words::{add_group, add_words};
|
||||||
use crate::dictionary::app_data_dir;
|
use crate::dictionary::app_data_dir;
|
||||||
use crate::import::ImportMessage::*;
|
use crate::import::ImportMessage::*;
|
||||||
use crate::lang::{WordData, WordGroup};
|
use crate::lang::{WordData, WordGroup};
|
||||||
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
|
use crate::notification::NotificationData::OpenFileError;
|
||||||
|
use crate::notification::{NotificationData, NotificationRequest};
|
||||||
use crate::styling::*;
|
use crate::styling::*;
|
||||||
use iced::widget::button::danger;
|
use iced::widget::button::danger;
|
||||||
use iced::widget::container::success;
|
use iced::widget::container::success;
|
||||||
@@ -25,6 +26,7 @@ use std::path::PathBuf;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"];
|
const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"];
|
||||||
|
|
||||||
@@ -275,9 +277,6 @@ impl ImportState {
|
|||||||
let mut row = Row::new();
|
let mut row = Row::new();
|
||||||
|
|
||||||
for default in DEFAULT_FIELDS {
|
for default in DEFAULT_FIELDS {
|
||||||
// if group.mapping.values().any(|name| name == default) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
row = row.push(
|
row = row.push(
|
||||||
button(text!("{default}")).on_press(SetupDefaultProperty(default.to_string())),
|
button(text!("{default}")).on_press(SetupDefaultProperty(default.to_string())),
|
||||||
);
|
);
|
||||||
@@ -365,41 +364,38 @@ impl ImportState {
|
|||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
},
|
},
|
||||||
|result: Result<ImportData, ()>| {
|
|result: Result<ImportData, NotificationData>| match result {
|
||||||
if let Ok(import) = result {
|
Ok(import) => RootMessage::Import(UpdateImport(import)),
|
||||||
RootMessage::Import(UpdateImport(import))
|
Err(err) => RootMessage::PushNotification(NotificationRequest::Error(err)),
|
||||||
} else {
|
|
||||||
RootMessage::None
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_import_file(path: PathBuf) -> Result<(), ()> {
|
fn extract_import_file(path: PathBuf) -> Result<(), NotificationData> {
|
||||||
let file = File::open(path).map_err(|_| ())?;
|
let file = File::open(path).map_err(|_| OpenFileError)?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
let mut archive = ZipArchive::new(reader).map_err(|_| ())?;
|
let mut archive = ZipArchive::new(reader).map_err(|_| OpenFileError)?;
|
||||||
let temp_file_path = app_data_dir().join("import");
|
let temp_file_path = app_data_dir().join("import");
|
||||||
let mut temp_file = OpenOptions::new()
|
let mut temp_file = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.open(&temp_file_path)
|
.open(&temp_file_path)
|
||||||
.map_err(|_| ())?;
|
.map_err(|_| OpenFileError)?;
|
||||||
if let Ok(mut file) = archive.by_name("collection.anki21") {
|
if let Ok(mut file) = archive.by_name("collection.anki21") {
|
||||||
std::io::copy(&mut file, &mut temp_file).map_err(|_| ())?;
|
std::io::copy(&mut file, &mut temp_file).map_err(|_| OpenFileError)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else if let Ok(mut file) = archive.by_name("collection.anki2") {
|
} else if let Ok(mut file) = archive.by_name("collection.anki2") {
|
||||||
std::io::copy(&mut file, &mut temp_file).map_err(|_| ())?;
|
std::io::copy(&mut file, &mut temp_file).map_err(|_| OpenFileError)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(())
|
Err(OpenFileError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn read_import_file() -> Result<ImportData, ()> {
|
fn read_import_file() -> Result<ImportData, NotificationData> {
|
||||||
let temp_file_path = app_data_dir().join("import");
|
let temp_file_path = app_data_dir().join("import");
|
||||||
let connection = Connection::open(&temp_file_path).map_err(|_| ())?;
|
let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?;
|
||||||
let data = load_groups(&connection);
|
let data = load_groups(&connection)?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
fn setup_mapping(&mut self, property_name: String) {
|
fn setup_mapping(&mut self, property_name: String) {
|
||||||
@@ -435,8 +431,8 @@ impl ImportState {
|
|||||||
async move {
|
async move {
|
||||||
spawn_blocking(move || {
|
spawn_blocking(move || {
|
||||||
let temp_file_path = app_data_dir().join("import");
|
let temp_file_path = app_data_dir().join("import");
|
||||||
let connection = Connection::open(&temp_file_path).map_err(|_| ())?;
|
let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?;
|
||||||
let import_list = get_words_of_group(&connection, group.id);
|
let import_list = get_words_of_group(&connection, group.id)?;
|
||||||
let mut words_list = Vec::with_capacity(import_list.len());
|
let mut words_list = Vec::with_capacity(import_list.len());
|
||||||
|
|
||||||
let map_indices = Self::get_mapping_indices(&group);
|
let map_indices = Self::get_mapping_indices(&group);
|
||||||
@@ -500,7 +496,10 @@ impl ImportState {
|
|||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
},
|
},
|
||||||
|_: Result<(), ()>| RootMessage::Import(ImportFinished),
|
|r: Result<(), NotificationData>| match r {
|
||||||
|
Ok(_) => RootMessage::Import(ImportFinished),
|
||||||
|
Err(err) => RootMessage::PushNotification(NotificationRequest::Error(err))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
use crate::data_provider::card_stats::load_stats_of_deck;
|
use crate::data_provider::card_stats::load_stats_of_deck;
|
||||||
use crate::data_provider::card_stats::update_stat_score;
|
use crate::data_provider::card_stats::update_stat_score;
|
||||||
use crate::data_provider::history::{push_note, HistoryItem};
|
use crate::data_provider::history::{push_note, HistoryItem};
|
||||||
use crate::AppState;
|
use crate::app::AppState;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rand::distr::weighted::WeightedIndex;
|
use rand::distr::weighted::WeightedIndex;
|
||||||
|
|||||||
+5
-107
@@ -17,120 +17,18 @@ mod sync;
|
|||||||
mod word;
|
mod word;
|
||||||
mod writing;
|
mod writing;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
|
pub mod app;
|
||||||
|
|
||||||
use crate::RootMessage::Keyboard;
|
use crate::app::start;
|
||||||
use crate::data_provider::card_sets::load_sets;
|
use crate::navigation::RootMessage;
|
||||||
use crate::data_provider::settings::get_setting;
|
|
||||||
use crate::data_provider::sqlite::default_connection;
|
|
||||||
use crate::data_provider::words::{load_word_groups, load_words};
|
|
||||||
use crate::lang::{DeckSettings, Id, WordData, WordGroup};
|
|
||||||
use crate::navigation::{AppSettings, RootMessage, ScreenState};
|
|
||||||
use crate::quiz::*;
|
use crate::quiz::*;
|
||||||
use chrono::NaiveDate;
|
|
||||||
use hashbrown::HashMap;
|
|
||||||
use iced::{Font, window};
|
|
||||||
use iced::{Subscription, Theme, keyboard};
|
|
||||||
use iced_core::Size;
|
|
||||||
use iced_core::window::Position;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use iced_core::window::settings::PlatformSpecific;
|
|
||||||
use mimalloc::MiMalloc;
|
use mimalloc::MiMalloc;
|
||||||
use rusqlite::Connection;
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static GLOBAL: MiMalloc = MiMalloc;
|
static GLOBAL: MiMalloc = MiMalloc;
|
||||||
|
|
||||||
const USER_FONT: Font = Font::with_name("Noto Sans JP");
|
|
||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
println!("Welcome to JapLearn");
|
start()
|
||||||
iced::application(ScreenState::boot, ScreenState::update, ScreenState::view)
|
|
||||||
.window(window_settings())
|
|
||||||
.subscription(subscription)
|
|
||||||
.title("JapLearn")
|
|
||||||
.settings(iced::Settings {
|
|
||||||
default_text_size: iced::Pixels(18.0),
|
|
||||||
..iced::Settings::default()
|
|
||||||
})
|
|
||||||
.font(include_bytes!("../noto.ttf"))
|
|
||||||
.default_font(USER_FONT)
|
|
||||||
.theme(Theme::GruvboxDark)
|
|
||||||
.run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
fn window_settings() -> window::Settings {
|
|
||||||
let mut settings = window::Settings {
|
|
||||||
position: Position::Centered,
|
|
||||||
min_size: Some(Size::new(700.0_f32, 700.0_f32)),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
settings.platform_specific = PlatformSpecific {
|
|
||||||
application_id: "JapLearn".to_string(),
|
|
||||||
override_redirect: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
settings
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscription(_state: &ScreenState) -> Subscription<RootMessage> {
|
|
||||||
keyboard::listen().map(Keyboard)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AppState {
|
|
||||||
pub dictionary: Vec<WordData>,
|
|
||||||
pub decks: Vec<DeckSettings>,
|
|
||||||
pub word_groups: Vec<WordGroup>,
|
|
||||||
pub connection: Connection,
|
|
||||||
pub sync_data: AppSettings,
|
|
||||||
pub activity: HashMap<Id, Vec<(NaiveDate, u32)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AppState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
dictionary: vec![],
|
|
||||||
decks: vec![],
|
|
||||||
word_groups: vec![],
|
|
||||||
connection: default_connection(),
|
|
||||||
sync_data: AppSettings {
|
|
||||||
key: None,
|
|
||||||
auto_web_fetch: false,
|
|
||||||
},
|
|
||||||
activity: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fill_state(state: &mut AppState) {
|
|
||||||
let list = load_words(&state.connection);
|
|
||||||
let sets = load_sets(&state.connection);
|
|
||||||
let groups = load_word_groups(&state.connection);
|
|
||||||
let setting = load_settings(&state.connection);
|
|
||||||
|
|
||||||
state.dictionary = list;
|
|
||||||
state.decks = sets;
|
|
||||||
state.word_groups = groups;
|
|
||||||
state.sync_data = setting
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_settings(connection: &Connection) -> AppSettings {
|
|
||||||
let key = get_setting("SYNC_KEY".to_string(), connection);
|
|
||||||
let fetch = get_setting("AUTO_WEB_FETCH".to_string(), connection)
|
|
||||||
.unwrap_or("false".to_string())
|
|
||||||
.parse::<bool>()
|
|
||||||
.unwrap();
|
|
||||||
AppSettings {
|
|
||||||
key,
|
|
||||||
auto_web_fetch: fetch,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -22,7 +22,7 @@ use crate::sync::{SyncMessage, SyncState};
|
|||||||
use crate::view_navigation;
|
use crate::view_navigation;
|
||||||
use crate::word::{WordMessage, WordState};
|
use crate::word::{WordMessage, WordState};
|
||||||
use crate::writing::{WritingMessage, WritingState};
|
use crate::writing::{WritingMessage, WritingState};
|
||||||
use crate::{fill_state, AppState};
|
use crate::app::{fill_state, AppState};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use iced::keyboard::Event;
|
use iced::keyboard::Event;
|
||||||
|
|||||||
@@ -132,6 +132,18 @@ impl NotificationManager {
|
|||||||
NotificationData::ImportError => {
|
NotificationData::ImportError => {
|
||||||
text!("Ошибка при импорте данных из Anki")
|
text!("Ошибка при импорте данных из Anki")
|
||||||
}
|
}
|
||||||
|
NotificationData::IncorrectAnkiFile => {
|
||||||
|
text!("Неподдерживаемый формат файла .apkg")
|
||||||
|
}
|
||||||
|
NotificationData::OpenFileError => {
|
||||||
|
text!("Ошибка открытия файла пакета")
|
||||||
|
}
|
||||||
|
NotificationData::VoiceVoxUnavailable => {
|
||||||
|
text!("Не получается подключиться к VoiceVox")
|
||||||
|
}
|
||||||
|
NotificationData::PermissionDenied => {
|
||||||
|
text!("У приложение недостаточно прав")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@@ -146,6 +158,10 @@ pub enum NotificationRequest {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum NotificationData {
|
pub enum NotificationData {
|
||||||
|
PermissionDenied,
|
||||||
DataLoaded,
|
DataLoaded,
|
||||||
ImportError,
|
ImportError,
|
||||||
|
IncorrectAnkiFile,
|
||||||
|
OpenFileError,
|
||||||
|
VoiceVoxUnavailable
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
use crate::lang::KanaSet;
|
use crate::lang::KanaSet;
|
||||||
use crate::navigation::Page::PreviousPage;
|
use crate::navigation::Page::PreviousPage;
|
||||||
use crate::navigation::{NavigatedPage, Page};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
use crate::quiz::QuizMessage::*;
|
use crate::quiz::QuizMessage::*;
|
||||||
use crate::styling::*;
|
use crate::styling::*;
|
||||||
use crate::{RootMessage, USER_FONT};
|
|
||||||
use iced::widget::*;
|
use iced::widget::*;
|
||||||
use iced::{Element, Fill, Task, alignment};
|
use iced::{Element, Fill, Task, alignment};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
|
use crate::app::USER_FONT;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct QuizState {
|
pub struct QuizState {
|
||||||
|
|||||||
+2
-2
@@ -2,9 +2,8 @@ use crate::data_provider::voice::get_voice;
|
|||||||
use crate::lang::Id;
|
use crate::lang::Id;
|
||||||
use crate::lang::{CardStatistics, DeckData, DeckSettings, WordData, WordOpenMode};
|
use crate::lang::{CardStatistics, DeckData, DeckSettings, WordData, WordOpenMode};
|
||||||
use crate::navigation::Page::PreviousPage;
|
use crate::navigation::Page::PreviousPage;
|
||||||
use crate::navigation::{KeyPressedPage, NavigatedPage, Page};
|
use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage};
|
||||||
use crate::styling::*;
|
use crate::styling::*;
|
||||||
use crate::{AppState, RootMessage};
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use iced::alignment::Horizontal::Center;
|
use iced::alignment::Horizontal::Center;
|
||||||
use iced::keyboard::key::Physical::Code;
|
use iced::keyboard::key::Physical::Code;
|
||||||
@@ -16,6 +15,7 @@ use rodio::MixerDeviceSink;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
pub struct RepetitionState {
|
pub struct RepetitionState {
|
||||||
state: Arc<Mutex<AppState>>,
|
state: Arc<Mutex<AppState>>,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::AppState;
|
|
||||||
use crate::data_provider::card_sets::{delete_set, update_deck};
|
use crate::data_provider::card_sets::{delete_set, update_deck};
|
||||||
use crate::data_provider::card_stats::{delete_stats, load_stats_of_deck};
|
use crate::data_provider::card_stats::{delete_stats, load_stats_of_deck};
|
||||||
use crate::lang::DeckSettings;
|
use crate::lang::DeckSettings;
|
||||||
@@ -15,6 +15,7 @@ use iced_core::{Padding, Theme, color};
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
pub struct RepetitionSettingsState {
|
pub struct RepetitionSettingsState {
|
||||||
set: DeckSettings,
|
set: DeckSettings,
|
||||||
|
|||||||
+2
-2
@@ -3,12 +3,11 @@ use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck};
|
|||||||
use crate::history::HistoryState;
|
use crate::history::HistoryState;
|
||||||
use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode};
|
use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode};
|
||||||
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
|
use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings};
|
||||||
use crate::navigation::{NavigatedPage, Page};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
use crate::repetition::RepetitionState;
|
use crate::repetition::RepetitionState;
|
||||||
use crate::repetition_settings::RepetitionSettingsState;
|
use crate::repetition_settings::RepetitionSettingsState;
|
||||||
use crate::repetitions::RepetitionsMessage::*;
|
use crate::repetitions::RepetitionsMessage::*;
|
||||||
use crate::styling::*;
|
use crate::styling::*;
|
||||||
use crate::{AppState, RootMessage};
|
|
||||||
use chrono::{Days, Local, Utc};
|
use chrono::{Days, Local, Utc};
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
pub use iced::widget::button::{Catalog, Style};
|
pub use iced::widget::button::{Catalog, Style};
|
||||||
@@ -25,6 +24,7 @@ use iced_core::svg::Handle;
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RepetitionsState {
|
pub struct RepetitionsState {
|
||||||
|
|||||||
+3
-2
@@ -1,14 +1,13 @@
|
|||||||
use crate::dictionary::DictionaryState;
|
use crate::dictionary::DictionaryState;
|
||||||
use crate::lang::{KanaSet, KanaType};
|
use crate::lang::{KanaSet, KanaType};
|
||||||
use crate::navigation::Page::*;
|
use crate::navigation::Page::*;
|
||||||
use crate::navigation::{NavigatedPage, Page};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
use crate::randomizer::RandomizerState;
|
use crate::randomizer::RandomizerState;
|
||||||
use crate::repetitions::RepetitionsState;
|
use crate::repetitions::RepetitionsState;
|
||||||
use crate::selector::SelectorMessage::ChangeMode;
|
use crate::selector::SelectorMessage::ChangeMode;
|
||||||
use crate::styling::*;
|
use crate::styling::*;
|
||||||
use crate::sync::SyncState;
|
use crate::sync::SyncState;
|
||||||
use crate::writing::WritingState;
|
use crate::writing::WritingState;
|
||||||
use crate::{AppState, QuizState, RootMessage};
|
|
||||||
use iced::widget::button::{Status, Style};
|
use iced::widget::button::{Status, Style};
|
||||||
use iced::widget::*;
|
use iced::widget::*;
|
||||||
use iced::{Element, Task, alignment};
|
use iced::{Element, Task, alignment};
|
||||||
@@ -17,6 +16,8 @@ use iced_core::Background;
|
|||||||
use iced_core::Color;
|
use iced_core::Color;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use crate::app::AppState;
|
||||||
|
use crate::quiz::QuizState;
|
||||||
|
|
||||||
pub struct SelectorState {
|
pub struct SelectorState {
|
||||||
pub set: KanaSet,
|
pub set: KanaSet,
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,9 @@
|
|||||||
use crate::data_provider::settings::{delete_settings, set_setting};
|
use crate::data_provider::settings::{delete_settings, set_setting};
|
||||||
use crate::data_provider::web_api::*;
|
use crate::data_provider::web_api::*;
|
||||||
use crate::navigation::Page::PreviousPage;
|
use crate::navigation::Page::PreviousPage;
|
||||||
use crate::navigation::{NavigatedPage, Page};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
use crate::styling::*;
|
use crate::styling::*;
|
||||||
use crate::sync::SyncMessage::*;
|
use crate::sync::SyncMessage::*;
|
||||||
use crate::{fill_state, AppState, RootMessage};
|
|
||||||
use iced::widget::button::danger;
|
use iced::widget::button::danger;
|
||||||
use iced::widget::container::rounded_box;
|
use iced::widget::container::rounded_box;
|
||||||
use iced::widget::{button, column, container, progress_bar, row, space, text, toggler};
|
use iced::widget::{button, column, container, progress_bar, row, space, text, toggler};
|
||||||
@@ -12,6 +11,7 @@ use iced::{Center, Element, Fill, Font, Length, Task};
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use crate::app::{fill_state, AppState};
|
||||||
use crate::notification::{NotificationRequest, NotificationData};
|
use crate::notification::{NotificationRequest, NotificationData};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
+2
-2
@@ -1,15 +1,15 @@
|
|||||||
use crate::data_provider::words::{delete_word, update_word};
|
use crate::data_provider::words::{delete_word, update_word};
|
||||||
use crate::lang::WordData;
|
use crate::lang::WordData;
|
||||||
use crate::navigation::Page::PreviousPage;
|
use crate::navigation::Page::PreviousPage;
|
||||||
use crate::navigation::{NavigatedPage, Page};
|
use crate::navigation::{NavigatedPage, Page, RootMessage};
|
||||||
use crate::styling::*;
|
use crate::styling::*;
|
||||||
use crate::word::WordMessage::*;
|
use crate::word::WordMessage::*;
|
||||||
use crate::{AppState, RootMessage};
|
|
||||||
use iced::widget::button::danger;
|
use iced::widget::button::danger;
|
||||||
use iced::widget::{button, column, row, rule, scrollable, text, text_input};
|
use iced::widget::{button, column, row, rule, scrollable, text, text_input};
|
||||||
use iced::{Element, Fill, Task};
|
use iced::{Element, Fill, Task};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WordState {
|
pub struct WordState {
|
||||||
|
|||||||
Reference in new issue
Block a user