diff options
| author | Joe Carstairs <me@joeac.net> | 2025-08-25 16:35:45 +0000 |
|---|---|---|
| committer | joeac <me@joeac.net> | 2025-08-25 16:35:45 +0000 |
| commit | fe85652e4d3e8906824baddaafd05d19105579b8 (patch) | |
| tree | 31b56e31cbf289a34ec1d6707becf3c60933b56f /schist_desktop_gui/src/gui/screens | |
| parent | d082a30de6a0cbd1fbe2ab5a4316db0891004129 (diff) | |
task-072
Co-authored-by: Joe Carstairs <jcarstairs@scottlogic.com>
Reviewed-on: https://git.joeac.net/joeac/schist/pulls/3
Co-authored-by: Joe Carstairs <me@joeac.net>
Co-committed-by: Joe Carstairs <me@joeac.net>
Diffstat (limited to 'schist_desktop_gui/src/gui/screens')
5 files changed, 274 insertions, 7 deletions
diff --git a/schist_desktop_gui/src/gui/screens/files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen.rs new file mode 100644 index 0000000..68a9556 --- /dev/null +++ b/schist_desktop_gui/src/gui/screens/files_screen.rs @@ -0,0 +1,36 @@ +mod update_files_screen; +mod view_files_screen; + +use std::{fmt::Debug, path::PathBuf}; + +use crate::DatabaseConnection; + +pub struct FilesScreen { + new_file_err: Option<(PathBuf, String)>, + file_paths: Vec<PathBuf>, + open_file_err: Option<(PathBuf, String)>, +} + +#[derive(Clone, Debug)] +pub enum Message { + ImportFromActualbudget, + NewFile, + OpenFile(std::path::PathBuf), + PickFile, +} + +#[derive(Debug)] +pub enum Action { + None, + OpenedFile(DatabaseConnection), +} + +impl<'a> FilesScreen { + pub fn new(file_paths: &'a [PathBuf]) -> Self { + Self { + file_paths: file_paths.to_vec(), + new_file_err: None, + open_file_err: None, + } + } +} 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 new file mode 100644 index 0000000..9c45d6e --- /dev/null +++ b/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs @@ -0,0 +1,125 @@ +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}, + paths::get_schist_database_directory, + traits::Component, + DatabaseConnection, +}; + +use super::{Action, FilesScreen, Message}; + +impl<'a> Component<'a, Message, Action> for FilesScreen { + fn update(&mut self, message: Message) -> Action { + match message { + Message::NewFile => match self.new_file() { + Some(Ok(file)) => Action::OpenedFile(file), + Some(Err(_)) | None => Action::None, + }, + + Message::OpenFile(file_path) => self + .open_file(file_path) + .map_or(Action::None, Action::OpenedFile), + + Message::PickFile => match self.pick_file() { + Some(Ok(file)) => Action::OpenedFile(file), + Some(Err(_)) | None => Action::None, + }, + + Message::ImportFromActualbudget => match self.import_from_actualbudget() { + Some(Ok(file)) => Action::OpenedFile(file), + Some(Err(_)) | 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.open_file_err = Some((path.clone(), err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + }) + }) + } + + 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.open_file_err = Some((file_path, err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + }) + } + + 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.new_file_err = Some((path.clone(), err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + }) + }) + } + + 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({ + self.open_file_err = Some((path, err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + 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.new_file_err = Some((path, err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + Err(err) + }), + None => None, + } + } +} diff --git a/schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs new file mode 100644 index 0000000..3cbd939 --- /dev/null +++ b/schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs @@ -0,0 +1,106 @@ +use std::path::PathBuf; + +use iced::widget::column; + +use crate::{ + gui::components::{panel_button, Text}, + style::*, + traits::Viewable, +}; + +use super::{FilesScreen, Message}; + +impl<'a> Viewable<'a, Message> for FilesScreen { + fn view(&'a self) -> iced::Element<'a, Message> { + let mut cols = Vec::new(); + if let Some(col) = self.view_new_file_err() { + cols.push(col); + }; + if let Some(col) = self.view_open_file_err() { + cols.push(col); + }; + cols.push(self.view_new_file_button()); + cols.push(self.view_open_file_button()); + cols.push(self.view_import_from_actualbudget_button()); + cols.append(&mut self.view_files()); + column(cols).padding(SPACING_LG).spacing(SPACING_MD).into() + } +} + +impl<'a> FilesScreen { + fn view_new_file_err(&'a self) -> Option<iced::Element<'a, Message>> { + self.new_file_err.as_ref().map(|(path, err)| { + let msg = format!("Failed to open new file {:#?}: {:#?}", path, err); + Text::new(&msg).danger().into() + }) + } + + fn view_open_file_err(&'a self) -> Option<iced::Element<'a, Message>> { + self.open_file_err + .as_ref() + .filter(|(file_path, _err)| !self.file_paths.iter().any(|fp| *fp == *file_path)) + .map(|(file_path, err)| { + let msg = format!("Failed to open new file {:#?}: {:#?}", file_path, err); + Text::new(&msg).danger().into() + }) + } + + fn view_files(&'a self) -> Vec<iced::Element<'a, Message>> { + self.file_paths + .iter() + .map(|f| { + view_file( + f, + self.open_file_err + .clone() + .filter(|(file, _err)| *file == *f) + .map(|(_file, err)| err), + ) + }) + .collect() + } + + fn view_new_file_button(&'a self) -> iced::Element<'a, Message> { + panel_button::<Text, Message>(Text::default("New file").into(), Message::NewFile).into() + } + + fn view_open_file_button(&'a self) -> iced::Element<'a, Message> { + panel_button::<Text, Message>(Text::default("Open file").into(), Message::PickFile).into() + } + + fn view_import_from_actualbudget_button(&'a self) -> iced::Element<'a, Message> { + panel_button::<Text, Message>( + Text::default("Import from Actualbudget").into(), + Message::ImportFromActualbudget, + ) + .into() + } +} + +fn view_file<'a>(file_path: &'a PathBuf, err: Option<String>) -> iced::Element<'a, Message> { + let mut cols: Vec<iced::Element<'a, Message>> = err + .map(|err| { + let msg = format!("Failed to open new file {:#?}: {:#?}", file_path, err); + vec![Text::new(&msg).danger().small().into()] + }) + .unwrap_or_else(Vec::new); + + cols.push( + Text::default(file_path.to_str().unwrap_or("")) + .small() + .weak() + .into(), + ); + + cols.push( + Text::default( + file_path + .file_name() + .map(|os_str| os_str.to_str().unwrap_or("[invalid Unicode]")) + .unwrap_or("[invalid filename]"), + ) + .into(), + ); + + panel_button(column(cols), Message::OpenFile(file_path.clone())).into() +} diff --git a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs index 0af3c28..00f4ec3 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs @@ -147,12 +147,12 @@ impl<'a> Component<'a, Message, Action> for MainScreen { } impl MainScreen { - pub fn new(buckets: Vec<Bucket>, accounts: Vec<Account>) -> Self { + pub fn new() -> Self { Self { - balances_view: BalancesView::new(buckets.clone()), - buckets: buckets.clone(), - buckets_view: BucketsView::new(buckets, "Hello, buckets!"), - transactions_view: TransactionsView::new(accounts, "Hello, transactions!"), + balances_view: BalancesView::new(Vec::new()), + buckets: Vec::new(), + buckets_view: BucketsView::new(Vec::new(), "Hello, buckets!"), + transactions_view: TransactionsView::new(Vec::new(), "Hello, transactions!"), active_view: View::Balances, view_navigation: Navigation::new(Some(View::Balances), View::views()), } diff --git a/schist_desktop_gui/src/gui/screens/main_screen/view.rs b/schist_desktop_gui/src/gui/screens/main_screen/view.rs index eb9c88e..2be427b 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/view.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/view.rs @@ -1,4 +1,4 @@ -use iced::Element; +use iced::{widget::Text, Element}; use crate::gui::components::navigation; @@ -43,6 +43,6 @@ impl View { impl<'a> Into<Element<'a, navigation::Message<View>>> for View { fn into(self) -> Element<'a, navigation::Message<View>> { - iced::widget::text(self.name()).into() + Text::new(self.name()).into() } } |
