diff --git a/src/data_provider/import.rs b/src/data_provider/import.rs new file mode 100644 index 0000000..609aa24 --- /dev/null +++ b/src/data_provider/import.rs @@ -0,0 +1,48 @@ +use rusqlite::Connection; +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct ImportData(Vec); + +#[derive(Clone, Debug)] +pub struct ImportGroup { + id: u64, + name: String, + fields: Vec, + mapping: HashMap, +} + +#[derive(Clone)] +pub struct ImportNote { + name: String, + tags: String, + fields: Vec, +} + +pub fn load_groups(connection: &Connection) -> ImportData { + let json_models: String = connection + .query_one("select models from col", (), |row| row.get(0)) + .unwrap(); + let raw: Value = serde_json::from_str(json_models.as_str()).unwrap(); + let sets = raw.as_object().unwrap(); + let mut total_data = ImportData(vec![]); + + for (_, collection) in sets { + let fields = collection["flds"] + .as_array() + .unwrap() + .iter() + .map(|x| x["name"].as_str().unwrap().to_string()) + .collect(); + let group = ImportGroup { + id: collection["id"].as_u64().unwrap(), + name: collection["name"].as_str().unwrap().to_string(), + fields, + mapping: Default::default(), + }; + println!("{:?}", group); + total_data.0.push(group); + } + total_data +} diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs index 4a507ae..acd0a77 100644 --- a/src/data_provider/mod.rs +++ b/src/data_provider/mod.rs @@ -6,3 +6,4 @@ pub(crate) mod sqlite; pub(crate) mod voice; pub(crate) mod words; pub(crate) mod web_api; +pub(crate) mod import; diff --git a/src/dictionary.rs b/src/dictionary.rs index a910516..6095013 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -264,7 +264,7 @@ impl NavigatedPage for DictionaryState { row![horizontal().width(8), self.words_list(),], row![button("Добавить слово").style(jl_button).on_press(NewWord), horizontal().width(Fill), - button("Импорт").style(jl_button).on_press(ToImport), + button("Импорт").style(text).on_press(ToImport), ] ] .spacing(5), diff --git a/src/import.rs b/src/import.rs index 01efb16..7ee258a 100644 --- a/src/import.rs +++ b/src/import.rs @@ -1,44 +1,164 @@ -use std::sync::{Arc, Mutex}; -use iced::{Element, Task}; -use iced::widget::scrollable; +use crate::data_provider::import::{load_groups, ImportData}; +use crate::dictionary::app_data_dir; +use crate::import::ImportMessage::*; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::styling::*; -use iced::widget::{column, row, button, text }; use crate::AppState; -use crate::import::ImportMessage::*; +use iced::widget::scrollable; +use iced::widget::{button, column, row, text}; +use iced::{Element, Task}; +use iced_core::Alignment::Center; +use rfd::AsyncFileDialog; +use rusqlite::Connection; +use std::fs::{File, OpenOptions}; +use std::io::BufReader; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use tokio::task::spawn_blocking; +use zip::ZipArchive; + #[derive(Clone)] pub struct ImportState { - state: Arc> + state: Arc>, + path: Option, + import_data: Option, + selected_index: usize, } + #[derive(Clone)] pub enum ImportMessage { - Back + Back, + SelectFile, + UpdateFile(PathBuf), + UpdateImport(ImportData), } impl NavigatedPage for ImportState { fn navigate(&self, message: &ImportMessage) -> Option { if let Back = message { - return Some(Page::PreviousPage) + return Some(Page::PreviousPage); } None } - fn navigated(&mut self) { - } + fn navigated(&mut self) {} fn update(&mut self, message: ImportMessage) -> Task { + match message { + Back => {} + UpdateFile(path) => { + self.path = Some(path); + return self.load_package(); + } + SelectFile => return Self::select_import_file(), + UpdateImport(import) => { + self.import_data = Some(import); + } + } Task::none() } fn view(&self) -> Element<'_, ImportMessage> { - back_overlay(scrollable(column![]).into(), Back) + back_overlay( + scrollable( + column![ + row![ + button("Выбрать файл").style(jl_button).on_press(SelectFile), + text!("{}", { + if let Some(path) = &self.path { + path.to_string_lossy().to_string() + } else { + "Файл не выбран".to_string() + } + }) + ] + .align_y(Center) + .spacing(DEFAULT_SPACING) + ] + .spacing(DEFAULT_SPACING), + ) + .into(), + Back, + ) } } impl ImportState { pub fn new(state: Arc>) -> ImportState { - ImportState{ - state + ImportState { + state, + path: None, + import_data: None, + selected_index: 0 } } -} + fn select_import_file() -> Task { + Task::perform( + async { + let file = AsyncFileDialog::new() + .add_filter("Anki", &["apkg"]) + .pick_file() + .await; + if let Some(file) = file { + return Some(file.path().into()); + } + None + }, + |result| { + if let Some(path) = result { + RootMessage::Import(UpdateFile(path)) + } else { + RootMessage::None + } + }, + ) + } + + pub fn load_package(&self) -> Task { + let path = self.path.clone().unwrap(); + Task::perform( + async { + spawn_blocking(move || { + Self::extract_import_file(path)?; + let data = Self::read_import_file()?; + return Ok(data); + }) + .await + .unwrap() + }, + |result: Result| { + if let Ok(import) = result { + RootMessage::Import(UpdateImport(import)) + } else { + RootMessage::None + } + }, + ) + } + + fn extract_import_file(path: PathBuf) -> Result<(), ()> { + let file = File::open(path).map_err(|_| ())?; + let reader = BufReader::new(file); + let mut archive = ZipArchive::new(reader).map_err(|_| ())?; + let db_file = archive.by_name("collection.anki2"); + if let Ok(mut file) = db_file { + let temp_file_path = app_data_dir().join("import"); + let mut temp_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&temp_file_path) + .map_err(|_| ())?; + std::io::copy(&mut file, &mut temp_file).map_err(|_| ())?; + Ok(()) + } else { + Err(()) + } + } + fn read_import_file() -> Result { + let temp_file_path = app_data_dir().join("import"); + let connection = Connection::open(&temp_file_path).map_err(|_| ())?; + let data = load_groups(&connection); + Ok(data) + } +}