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, Task}; use schist_models::{Account, AccountTransfer, Bucket, Transaction}; use schist_queries::{ account_transfers::get_all_account_transfers, accounts::get_all_accounts, buckets::get_all_buckets, transactions::get_all_transactions, }; 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, 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>), RefetchedAccountTransfers(FetchResult>), RefetchedBuckets(FetchResult>), RefetchedTransactions(FetchResult>), } pub type FetchResult = std::result::Result; #[derive(Clone, Debug)] pub struct FetchError { pub message: String, } impl FetchError { pub fn new(err: anyhow::Error) -> Self { Self { message: format!("{}", err), } } } pub struct Gui { active_screen: Screen, connection: Option, files_screen: FilesScreen, main_screen: MainScreen, config: Config, } impl Gui { pub fn new(cache: &Cache) -> (Self, iced::Task) { let file_paths = get_file_paths(); let mut gui = Self { active_screen: Screen::FilesScreen, connection: None, files_screen: FilesScreen::new(&file_paths), main_screen: MainScreen::new(), config: Config::default(), }; 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)) } } impl Gui { pub fn view<'a>(&'a self) -> Element<'a, Message> { match self.active_screen { Screen::FilesScreen => self.files_screen.view().map(Message::FilesScreenMessage), Screen::MainScreen => self.main_screen.view().map(Message::MainScreenMessage), } } pub fn update(&mut self, message: Message) -> iced::Task { match message { Message::Event(iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { key, modifiers, .. })) => { let key_bind = KeyBind::from((key.clone(), modifiers)); if let Some(command) = self .config .keyboard .shortcuts() .iter() .find_map(|shortcut| shortcut.execute(&key_bind)) { match command { crate::shortcut::Command::Activate => match self.active_screen { Screen::FilesScreen => self .update_files_screen(files_screen::Message::ActivateSelectedOption), Screen::MainScreen => iced::Task::none(), }, crate::shortcut::Command::GotoNextView => { self.main_screen.update(main_screen::Message::SelectView( self.main_screen.active_view.next(), )); iced::Task::none() } crate::shortcut::Command::GotoPrevView => { self.main_screen.update(main_screen::Message::SelectView( self.main_screen.active_view.prev(), )); iced::Task::none() } crate::shortcut::Command::GotoView(view) => { self.main_screen .update(main_screen::Message::SelectView(view)); iced::Task::none() } crate::shortcut::Command::GotoNextItem => { match self.active_screen { Screen::FilesScreen => { self.files_screen.update(files_screen::Message::NextOption); } Screen::MainScreen => { self.main_screen.update(main_screen::Message::NextItem); } }; iced::Task::none() } crate::shortcut::Command::GotoPrevItem => { match self.active_screen { Screen::FilesScreen => { self.files_screen.update(files_screen::Message::PrevOption); } Screen::MainScreen => { self.main_screen.update(main_screen::Message::PrevItem); } }; iced::Task::none() } crate::shortcut::Command::ExpandItem => { self.main_screen.update(main_screen::Message::ExpandItem); iced::Task::none() } crate::shortcut::Command::CollapseItem => { self.main_screen.update(main_screen::Message::CollapseItem); iced::Task::none() } } } else { iced::Task::none() } } Message::Event(_) => iced::Task::none(), Message::MainScreenMessage(message) => match self.main_screen.update(message) { main_screen::Action::None => iced::Task::none(), }, Message::RefetchedBuckets(buckets) => { match buckets { Ok(buckets) => { self.main_screen .update(main_screen::Message::SetBuckets(buckets)); } Err(err) => println!("Failed to get buckets: {:?}", err.message), }; iced::Task::none() } Message::RefetchedAccounts(accounts) => { match accounts { Ok(accounts) => { self.main_screen .update(main_screen::Message::SetAccounts(accounts)); } Err(err) => println!("Failed to get accounts: {:?}", err.message), } iced::Task::none() } Message::RefetchedAccountTransfers(account_transfers) => { match account_transfers { Ok(account_transfers) => { self.main_screen .update(main_screen::Message::SetAccountTransfers(account_transfers)); } Err(err) => println!("Failed to get account transfers: {:?}", err.message), } iced::Task::none() } Message::RefetchedTransactions(transactions) => { match transactions { Ok(transactions) => { self.main_screen .update(main_screen::Message::SetTransactions(transactions)); } Err(err) => println!("Failed to get transactions: {:?}", err.message), }; 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 { 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::OpenFile(path) => self.open_file(&path), files_screen::Action::PickFile => self.pick_file(), } } fn open_file(&mut self, file_path: &PathBuf) -> Task { 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 { let buckets = get_all_buckets(&mut connection).map_err(FetchError::new); let accounts = get_all_accounts(&mut connection).map_err(FetchError::new); let account_transfers = get_all_account_transfers(&mut connection).map_err(FetchError::new); let transactions = get_all_transactions(&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)), self.update(Message::RefetchedAccountTransfers(account_transfers)), self.update(Message::RefetchedTransactions(transactions)), ]) } fn new_file(&mut self) -> Task { 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 { 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 { 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 { 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(), } } } pub fn subscription(_state: &Gui) -> iced::Subscription { iced::event::listen().map(Message::Event) }