diff options
Diffstat (limited to 'schist_desktop_gui')
| -rw-r--r-- | schist_desktop_gui/Cargo.toml | 3 | ||||
| -rw-r--r-- | schist_desktop_gui/src/cache.rs | 83 | ||||
| -rw-r--r-- | schist_desktop_gui/src/dialog/new_file_dialog.rs | 6 | ||||
| -rw-r--r-- | schist_desktop_gui/src/dialog/pick_file_dialog.rs | 6 | ||||
| -rw-r--r-- | schist_desktop_gui/src/gui.rs | 128 | ||||
| -rw-r--r-- | schist_desktop_gui/src/gui/screens.rs | 2 | ||||
| -rw-r--r-- | schist_desktop_gui/src/gui/screens/files_screen.rs | 16 | ||||
| -rw-r--r-- | schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs | 123 | ||||
| -rw-r--r-- | schist_desktop_gui/src/main.rs | 7 |
9 files changed, 243 insertions, 131 deletions
diff --git a/schist_desktop_gui/Cargo.toml b/schist_desktop_gui/Cargo.toml index 0ee5764..e5937d8 100644 --- a/schist_desktop_gui/Cargo.toml +++ b/schist_desktop_gui/Cargo.toml @@ -17,3 +17,6 @@ schist_fakes = { workspace = true } schist_models = { workspace = true } schist_queries = { workspace = true } schist_schema = { workspace = true } +serde = { workspace = true } +toml = { workspace = true } +unwrap-infallible = { workspace = true } diff --git a/schist_desktop_gui/src/cache.rs b/schist_desktop_gui/src/cache.rs new file mode 100644 index 0000000..51e2c55 --- /dev/null +++ b/schist_desktop_gui/src/cache.rs @@ -0,0 +1,83 @@ +use std::{ + fs::{create_dir_all, exists, read, write, File}, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; + +const SCHIST_CACHE_DIR: &'static str = "schist"; +const SCHIST_CACHE_FILENAME: &'static str = "schist_cache.toml"; + +#[derive(Default, Deserialize, Serialize)] +pub struct Cache { + pub last_open_file: Option<String>, +} + +pub struct CacheKeys { + pub last_open_file: &'static str, +} + +pub const CACHE_KEYS: CacheKeys = CacheKeys { + last_open_file: "last_open_file", +}; + +pub fn get_cache() -> Result<Cache> { + const CONTEXT: &'static str = "Failed to get cache"; + let cache_filepath = get_and_touch_cache_filepath().context(CONTEXT)?; + let cache_toml = read(&cache_filepath).context(CONTEXT)?; + let cache_table: toml::Table = toml::from_slice(&cache_toml).context(CONTEXT)?; + Ok(Cache { + last_open_file: cache_table + .get(&CACHE_KEYS.last_open_file.to_string()) + .map(|o| o.as_str().map(str::to_string)) + .flatten(), + }) +} + +pub fn put_to_cache<T>(key: &str, value: T) -> Result<()> +where + toml::Value: From<T>, +{ + const CONTEXT: &'static str = "Failed to save cache"; + let cache_filepath = get_and_touch_cache_filepath().context(CONTEXT)?; + let curr_cache_toml = read(&cache_filepath).context(CONTEXT)?; + let mut cache: toml::Table = toml::from_slice(&curr_cache_toml).context(CONTEXT)?; + cache.insert(key.to_string(), value.into()); + let new_cache_toml = toml::to_string(&cache).context(CONTEXT)?; + write(cache_filepath, new_cache_toml).context(CONTEXT)?; + Ok(()) +} + +pub fn remove_from_cache(key: &str) -> Result<()> { + const CONTEXT: &'static str = "Failed to remove key from cache"; + let cache_filepath = get_and_touch_cache_filepath().context(CONTEXT)?; + let curr_cache_toml = read(&cache_filepath).context(CONTEXT)?; + let mut cache: toml::Table = toml::from_slice(&curr_cache_toml).context(CONTEXT)?; + cache.remove(key); + let new_cache_toml = toml::to_string(&cache).context(CONTEXT)?; + write(cache_filepath, new_cache_toml).context(CONTEXT)?; + Ok(()) +} + +fn get_and_touch_cache_filepath() -> Result<PathBuf, anyhow::Error> { + let cache_dir = get_and_make_schist_cache_dir()?; + let cache_filepath = cache_dir.join(Path::new(SCHIST_CACHE_FILENAME)); + let does_cache_file_exist = exists(cache_filepath.clone()) + .context("Failed to determine the existence of Schist's cache file")?; + if !does_cache_file_exist { + File::create(cache_filepath.clone()).context("Failed to create a new cache file")?; + } + Ok(cache_filepath) +} + +fn get_and_make_schist_cache_dir() -> anyhow::Result<PathBuf> { + let cache_dir = dirs::cache_dir().context("Failed to get app cache directory")?; + let schist_cache_dir = cache_dir.join(Path::new(SCHIST_CACHE_DIR)); + create_dir_all(&schist_cache_dir).context(format!( + "Failed to create app cache directory at {}", + schist_cache_dir.display() + ))?; + println!("Found app cache directory at {:?}", schist_cache_dir); + Ok(schist_cache_dir) +} diff --git a/schist_desktop_gui/src/dialog/new_file_dialog.rs b/schist_desktop_gui/src/dialog/new_file_dialog.rs index b950f68..0a148e1 100644 --- a/schist_desktop_gui/src/dialog/new_file_dialog.rs +++ b/schist_desktop_gui/src/dialog/new_file_dialog.rs @@ -1,6 +1,6 @@ -use crate::{database_connection::DatabaseConnectionResult, DatabaseConnection}; +use std::path::PathBuf; -pub fn new_file_dialog<P>(starting_directory: Option<P>) -> Option<DatabaseConnectionResult> +pub fn new_file_dialog<P>(starting_directory: Option<P>) -> Option<PathBuf> where P: AsRef<std::path::Path>, { @@ -11,5 +11,5 @@ where if let Some(starting_directory) = starting_directory { file_dialog = file_dialog.set_directory(starting_directory); } - file_dialog.save_file().map(DatabaseConnection::establish) + file_dialog.save_file() } diff --git a/schist_desktop_gui/src/dialog/pick_file_dialog.rs b/schist_desktop_gui/src/dialog/pick_file_dialog.rs index 53d36c6..fc1a330 100644 --- a/schist_desktop_gui/src/dialog/pick_file_dialog.rs +++ b/schist_desktop_gui/src/dialog/pick_file_dialog.rs @@ -1,6 +1,6 @@ -use crate::{database_connection::DatabaseConnectionResult, DatabaseConnection}; +use std::path::PathBuf; -pub fn pick_file_dialog<P>(starting_directory: Option<P>) -> Option<DatabaseConnectionResult> +pub fn pick_file_dialog<P>(starting_directory: Option<P>) -> Option<PathBuf> where P: AsRef<std::path::Path>, { @@ -10,5 +10,5 @@ where if let Some(starting_directory) = starting_directory { file_dialog = file_dialog.set_directory(starting_directory); } - file_dialog.pick_file().map(DatabaseConnection::establish) + file_dialog.pick_file() } diff --git a/schist_desktop_gui/src/gui.rs b/schist_desktop_gui/src/gui.rs index eb2fce9..26f147b 100644 --- a/schist_desktop_gui/src/gui.rs +++ b/schist_desktop_gui/src/gui.rs @@ -1,22 +1,33 @@ pub mod components; +use actualbudget_to_schist_transformer::schist_state::{export_schist_state, SchistState}; +use itertools::Itertools; pub mod screens; +use std::{path::PathBuf, str::FromStr}; + use diesel::SqliteConnection; -use iced::Element; +use iced::{Element, Task}; use schist_models::{Account, Bucket}; use schist_queries::{accounts::get_all_accounts, buckets::get_all_buckets}; use crate::{ + cache::{put_to_cache, remove_from_cache, CACHE_KEYS}, config::Config, + dialog::{import_from_actualbudget_dialog, new_file_dialog, pick_file_dialog}, gui::screens::{files_screen, main_screen, FilesScreen, MainScreen, Screen}, - paths::get_file_paths, + paths::{get_file_paths, get_schist_database_directory}, shortcut::KeyBind, traits::{Component, Viewable}, + Cache, DatabaseConnection, }; #[derive(Clone, Debug)] pub enum Message { Event(iced::Event), + FailedToCreateFile(String), + FailedToOpenFile(PathBuf, String), + FailedToPickFile(String), + FailedToImportFromActualbudget(String), FilesScreenMessage(files_screen::Message), MainScreenMessage(main_screen::Message), RefetchedAccounts(FetchResult<Vec<Account>>), @@ -47,16 +58,24 @@ pub struct Gui { } impl Gui { - pub fn new() -> (Self, iced::Task<Message>) { + pub fn new(cache: &Cache) -> (Self, iced::Task<Message>) { let file_paths = get_file_paths(); - let gui = Self { + let mut gui = Self { active_screen: Screen::FilesScreen, connection: None, files_screen: FilesScreen::new(&file_paths), main_screen: MainScreen::new(), config: Config::default(), }; - (gui, iced::Task::none()) + + let mut tasks = Vec::new(); + + if let Some(last_open_file) = &cache.last_open_file { + let Ok(last_open_file) = PathBuf::from_str(&last_open_file); + tasks.push(gui.open_file(&last_open_file)); + } + + (gui, Task::batch(tasks)) } } @@ -167,23 +186,102 @@ impl Gui { iced::Task::none() } Message::FilesScreenMessage(message) => self.update_files_screen(message), + Message::FailedToCreateFile(err) => { + self.update_files_screen(files_screen::Message::ReportFailedToCreateFile(err)) + } + Message::FailedToOpenFile(path, err) => { + self.update_files_screen(files_screen::Message::ReportFailedToOpenFile(path, err)) + } + Message::FailedToImportFromActualbudget(err) => self.update_files_screen( + files_screen::Message::ReportFailedToImportFromActualbudget(err), + ), + Message::FailedToPickFile(err) => { + self.update_files_screen(files_screen::Message::ReportFailedToPickFile(err)) + } } } fn update_files_screen(&mut self, message: files_screen::Message) -> iced::Task<Message> { match self.files_screen.update(message) { + files_screen::Action::ImportFromActualbudget => self.import_from_actualbudget(), + files_screen::Action::NewFile => self.new_file(), files_screen::Action::None => iced::Task::none(), - files_screen::Action::OpenedFile(sqlite_connection) => { - let mut connection = sqlite_connection.connection; - let buckets = get_all_buckets(&mut connection).map_err(FetchError::new); - let accounts = get_all_accounts(&mut connection).map_err(FetchError::new); - self.connection = Some(connection); - self.active_screen = Screen::MainScreen; - iced::Task::batch(vec![ - self.update(Message::RefetchedBuckets(buckets)), - self.update(Message::RefetchedAccounts(accounts)), - ]) + files_screen::Action::OpenFile(path) => self.open_file(&path), + files_screen::Action::PickFile => self.pick_file(), + } + } + + fn open_file(&mut self, file_path: &PathBuf) -> Task<Message> { + let sqlite_connection = DatabaseConnection::establish(file_path.to_path_buf()).ok(); + if let Err((path, err)) = sqlite_connection { + return Task::done(Message::FailedToOpenFile(path, err.chain().join("\n | "))); + } + self.on_new_connection(sqlite_connection.unwrap().connection, file_path) + } + + fn on_new_connection( + &mut self, + mut connection: SqliteConnection, + path: &PathBuf, + ) -> Task<Message> { + let buckets = get_all_buckets(&mut connection).map_err(FetchError::new); + let accounts = get_all_accounts(&mut connection).map_err(FetchError::new); + self.connection = Some(connection); + self.active_screen = Screen::MainScreen; + if let Some(path) = path.to_str() { + let _ = put_to_cache(CACHE_KEYS.last_open_file, path); + } else { + let _ = remove_from_cache(CACHE_KEYS.last_open_file); + } + iced::Task::batch(vec![ + self.update(Message::RefetchedBuckets(buckets)), + self.update(Message::RefetchedAccounts(accounts)), + ]) + } + + fn new_file(&mut self) -> Task<Message> { + match new_file_dialog(get_schist_database_directory().ok()) { + Some(path) => self.open_file(&path), + None => Task::none(), + } + } + + fn new_file_with_state(&mut self, state: &SchistState) -> Task<Message> { + match new_file_dialog(get_schist_database_directory().ok()) { + Some(path) => { + let sqlite_connection = DatabaseConnection::establish(path.clone()).ok(); + if let Err((path, err)) = sqlite_connection { + return Task::done(Message::FailedToOpenFile(path, err.chain().join("\n | "))); + } + let mut db_conn = sqlite_connection.unwrap().connection; + export_schist_state(state, &mut db_conn).map_or_else( + |err| Task::done(Message::FailedToCreateFile(err.chain().join("\n | "))), + move |()| self.on_new_connection(db_conn, &path), + ) } + None => Task::none(), + } + } + + fn import_from_actualbudget(&mut self) -> Task<Message> { + match import_from_actualbudget_dialog() { + Some(Ok(state)) => self.new_file_with_state(&state), + Some(Err((_path, err))) => Task::done(Message::FailedToImportFromActualbudget( + err.chain().join("\n | "), + )), + None => Task::none(), + } + } + + fn pick_file(&mut self) -> Task<Message> { + match pick_file_dialog(get_schist_database_directory().ok()) { + Some(path) => match DatabaseConnection::establish(path.clone()).ok() { + Ok(db_conn) => self.on_new_connection(db_conn.connection, &path), + Err((_path, err)) => { + Task::done(Message::FailedToPickFile(err.chain().join("\n | "))) + } + }, + None => Task::none(), } } } diff --git a/schist_desktop_gui/src/gui/screens.rs b/schist_desktop_gui/src/gui/screens.rs index abca2b6..5acf73b 100644 --- a/schist_desktop_gui/src/gui/screens.rs +++ b/schist_desktop_gui/src/gui/screens.rs @@ -4,7 +4,7 @@ pub mod main_screen; pub use files_screen::FilesScreen; pub use main_screen::MainScreen; -#[derive(Default)] +#[derive(Clone, Default)] pub enum Screen { #[default] FilesScreen, diff --git a/schist_desktop_gui/src/gui/screens/files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen.rs index 9757355..9700e7a 100644 --- a/schist_desktop_gui/src/gui/screens/files_screen.rs +++ b/schist_desktop_gui/src/gui/screens/files_screen.rs @@ -3,13 +3,10 @@ mod view_files_screen; use std::{fmt::Debug, path::PathBuf}; -use crate::{ - gui::components::{navigation, Navigation, Panel}, - DatabaseConnection, -}; +use crate::gui::components::{navigation, Navigation, Panel}; pub struct FilesScreen { - options: Navigation<Panel<OptionId>>, + pub options: Navigation<Panel<OptionId>>, } #[derive(Clone, Debug, PartialEq)] @@ -23,6 +20,10 @@ pub enum OptionId { #[derive(Clone, Debug, PartialEq)] pub enum Message { NavigationMessage(navigation::Message<Panel<OptionId>>), + ReportFailedToCreateFile(String), + ReportFailedToImportFromActualbudget(String), + ReportFailedToPickFile(String), + ReportFailedToOpenFile(PathBuf, String), ActivateSelectedOption, NextOption, PrevOption, @@ -30,8 +31,11 @@ pub enum Message { #[derive(Debug)] pub enum Action { + ImportFromActualbudget, + NewFile, None, - OpenedFile(DatabaseConnection), + OpenFile(PathBuf), + PickFile, } impl<'a> FilesScreen { diff --git a/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs index aa47f56..1339138 100644 --- a/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs +++ b/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs @@ -1,13 +1,6 @@ -use actualbudget_to_schist_transformer::schist_state::{export_schist_state, SchistState}; -use itertools::Itertools; - use crate::{ - database_connection::DatabaseConnectionResult, - dialog::{import_from_actualbudget_dialog, new_file_dialog, pick_file_dialog}, gui::components::{navigation, Panel}, - paths::get_schist_database_directory, traits::Component, - DatabaseConnection, }; use super::{Action, FilesScreen, Message, OptionId}; @@ -24,92 +17,31 @@ impl<'a> Component<'a, Message, Action> for FilesScreen { } None => Action::None, }, - } - } -} - -impl FilesScreen { - fn pick_file( - &mut self, - ) -> Option<Result<DatabaseConnection, (std::path::PathBuf, anyhow::Error)>> { - pick_file_dialog(get_schist_database_directory().ok()).map(|db_connection_result| { - db_connection_result.ok().inspect_err(|(_path, err)| { - self.report_err( - super::OptionId::PickFile, - format!("{}", err.chain().join("\n | ")).as_str(), - "Failed to open file", - ); - }) - }) - } - - fn open_file( - &mut self, - file_path: std::path::PathBuf, - ) -> Result<DatabaseConnection, (std::path::PathBuf, anyhow::Error)> { - DatabaseConnection::establish(file_path.clone()) - .ok() - .inspect_err(|(_path, err)| { - self.report_err( - OptionId::OpenFile(file_path), - format!("{}", err.chain().join("\n | ")).as_str(), - "Failed to open file", - ); - }) - } - - fn new_file( - &mut self, - ) -> Option<Result<DatabaseConnection, (std::path::PathBuf, anyhow::Error)>> { - new_file_dialog(get_schist_database_directory().ok()).map(|result| { - result.ok().inspect_err(|(_path, err)| { - self.report_err( - OptionId::NewFile, - "Failed to create new file", - format!("{}", err.chain().join("\n | ")).as_str(), - ); - }) - }) - } - - fn import_from_actualbudget(&mut self) -> Option<anyhow::Result<DatabaseConnection>> { - match import_from_actualbudget_dialog() { - Some(Ok(state)) => self.new_file_with_state(&state), - Some(Err((_path, err))) => Some({ + Message::ReportFailedToOpenFile(path, err) => { + self.report_err(OptionId::OpenFile(path), &err, "Failed to open file"); + Action::None + } + Message::ReportFailedToCreateFile(err) => { + self.report_err(OptionId::NewFile, &err, "Failed to create new file"); + Action::None + } + Message::ReportFailedToImportFromActualbudget(err) => { self.report_err( OptionId::ImportFromActualbudget, + &err, "Failed to import from Actualbudget", - format!("{}", err.chain().join("\n | ")).as_str(), - ); - Err(err) - }), - None => None, - } - } - - fn new_file_with_state( - &mut self, - state: &SchistState, - ) -> Option<anyhow::Result<DatabaseConnection>> { - match new_file_dialog(get_schist_database_directory().ok()) - .map(DatabaseConnectionResult::ok) - { - Some(Ok(mut file)) => Some({ - export_schist_state(state, &mut file.connection).ok()?; - Ok(file) - }), - Some(Err((_path, err))) => Some({ - self.report_err( - OptionId::NewFile, - format!("{}", err.chain().join("\n | ")).as_str(), - "Failed to create new file", ); - Err(err) - }), - None => None, + Action::None + } + Message::ReportFailedToPickFile(err) => { + self.report_err(OptionId::PickFile, &err, "Failed to choose file"); + Action::None + } } } +} +impl FilesScreen { fn report_err(&mut self, id: OptionId, err: &str, dialog_title: &str) { let old_panel = self.options.find(|o| o.id == id); let file_panel_with_err = Panel::new( @@ -134,33 +66,22 @@ impl FilesScreen { Panel { id: OptionId::NewFile, .. - } => match self.new_file() { - Some(Ok(file)) => Action::OpenedFile(file), - Some(Err(_)) | None => Action::None, - }, + } => Action::NewFile, Panel { id: OptionId::OpenFile(file_path), .. - } => self - .open_file(file_path.clone()) - .map_or(Action::None, Action::OpenedFile), + } => Action::OpenFile(file_path), Panel { id: OptionId::PickFile, .. - } => match self.pick_file() { - Some(Ok(file)) => Action::OpenedFile(file), - Some(Err(_)) | None => Action::None, - }, + } => Action::PickFile, Panel { id: OptionId::ImportFromActualbudget, .. - } => match self.import_from_actualbudget() { - Some(Ok(file)) => Action::OpenedFile(file), - Some(Err(_)) | None => Action::None, - }, + } => Action::ImportFromActualbudget, }, navigation::Action::SelectOption(_) | navigation::Action::None => Action::None, } diff --git a/schist_desktop_gui/src/main.rs b/schist_desktop_gui/src/main.rs index 6dd2622..a5749d3 100644 --- a/schist_desktop_gui/src/main.rs +++ b/schist_desktop_gui/src/main.rs @@ -1,3 +1,4 @@ +mod cache; mod config; mod database_connection; mod dialog; @@ -10,22 +11,24 @@ mod theme; mod traits; mod window_settings; +pub use cache::Cache; pub use database_connection::DatabaseConnection; use anyhow::Context; use crate::{ - config::Config, gui::Gui, settings::make_settings, theme::make_theme, + cache::get_cache, config::Config, gui::Gui, settings::make_settings, theme::make_theme, window_settings::make_window_settings, }; fn main() -> anyhow::Result<()> { + let cache = get_cache().unwrap_or_else(|_| Cache::default()); let config = Config::default(); iced::application("Schist", Gui::update, Gui::view) .settings(make_settings(&config.clone())) .subscription(gui::subscription) .theme(make_theme) .window(make_window_settings(&config.clone())) - .run_with(Gui::new) + .run_with(move || Gui::new(&cache)) .context("Failed to run Schist GUI") } |
