summaryrefslogtreecommitdiff
path: root/schist_desktop_gui/src
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2025-08-25 16:35:45 +0000
committerjoeac <me@joeac.net>2025-08-25 16:35:45 +0000
commitfe85652e4d3e8906824baddaafd05d19105579b8 (patch)
tree31b56e31cbf289a34ec1d6707becf3c60933b56f /schist_desktop_gui/src
parentd082a30de6a0cbd1fbe2ab5a4316db0891004129 (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')
-rw-r--r--schist_desktop_gui/src/connection.rs20
-rw-r--r--schist_desktop_gui/src/database_connection.rs71
-rw-r--r--schist_desktop_gui/src/dialog.rs7
-rw-r--r--schist_desktop_gui/src/dialog/import_from_actualbudget_dialog.rs26
-rw-r--r--schist_desktop_gui/src/dialog/new_file_dialog.rs15
-rw-r--r--schist_desktop_gui/src/dialog/pick_file_dialog.rs14
-rw-r--r--schist_desktop_gui/src/gui.rs50
-rw-r--r--schist_desktop_gui/src/gui/components.rs2
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs3
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs40
-rw-r--r--schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs11
-rw-r--r--schist_desktop_gui/src/gui/components/buckets_view.rs4
-rw-r--r--schist_desktop_gui/src/gui/components/button.rs21
-rw-r--r--schist_desktop_gui/src/gui/components/text.rs94
-rw-r--r--schist_desktop_gui/src/gui/components/transactions_view.rs4
-rw-r--r--schist_desktop_gui/src/gui/screens.rs5
-rw-r--r--schist_desktop_gui/src/gui/screens/files_screen.rs36
-rw-r--r--schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs125
-rw-r--r--schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs106
-rw-r--r--schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs10
-rw-r--r--schist_desktop_gui/src/gui/screens/main_screen/view.rs4
-rw-r--r--schist_desktop_gui/src/main.rs33
-rw-r--r--schist_desktop_gui/src/paths.rs56
-rw-r--r--schist_desktop_gui/src/style.rs4
-rw-r--r--schist_desktop_gui/src/theme.rs68
25 files changed, 717 insertions, 112 deletions
diff --git a/schist_desktop_gui/src/connection.rs b/schist_desktop_gui/src/connection.rs
deleted file mode 100644
index 5a88083..0000000
--- a/schist_desktop_gui/src/connection.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-use std::{fs::create_dir_all, path::Path};
-
-use anyhow::Context;
-use diesel::{Connection, SqliteConnection};
-
-const SCHIST_DATA_DIR: &'static str = "schist";
-const SCHIST_DATABASE_FILENAME: &'static str = "schist.sqlite";
-
-pub fn establish_connection() -> anyhow::Result<SqliteConnection> {
- let data_dir = dirs::data_dir().context("Failed to get local app data directory")?;
- let schist_local_dir = data_dir.join(Path::new(SCHIST_DATA_DIR));
- create_dir_all(&schist_local_dir)
- .context(format!("Failed to create local files directory at {}", schist_local_dir.display()))?;
- let db_path = schist_local_dir.join(Path::new(SCHIST_DATABASE_FILENAME));
- let db_path = db_path
- .to_str()
- .context("Failed to construct database path")?;
- Ok(SqliteConnection::establish(db_path)
- .with_context(|| format!("Failed to connect to database at {}", db_path))?)
-}
diff --git a/schist_desktop_gui/src/database_connection.rs b/schist_desktop_gui/src/database_connection.rs
new file mode 100644
index 0000000..99fbe2d
--- /dev/null
+++ b/schist_desktop_gui/src/database_connection.rs
@@ -0,0 +1,71 @@
+use std::{fmt, path::PathBuf};
+
+use anyhow::Context;
+use diesel::{Connection, SqliteConnection};
+use diesel_migrations::MigrationHarness;
+use schist_models::migrations::MIGRATIONS;
+
+pub struct DatabaseConnection {
+ pub connection: diesel::SqliteConnection,
+ pub file_path: PathBuf,
+}
+
+impl DatabaseConnection {
+ pub fn establish(file_path: PathBuf) -> DatabaseConnectionResult {
+ DatabaseConnectionResult {
+ connection: Self::establish_sqlite_connection(&file_path),
+ file_path: file_path,
+ }
+ }
+
+ fn establish_sqlite_connection(file_path: &PathBuf) -> anyhow::Result<SqliteConnection> {
+ let file_path_str = file_path
+ .to_str()
+ .with_context(|| format!("Failed to get database path from {:?}", file_path))?;
+ let mut connection = diesel::SqliteConnection::establish(&file_path_str)
+ .with_context(|| format!("Failed to connect to database at {}", file_path_str))?;
+ connection
+ .run_pending_migrations(MIGRATIONS)
+ .map_err(anyhow::Error::from_boxed)
+ .with_context(|| {
+ format!(
+ "Failed to run pending migrations on database at {}",
+ file_path_str
+ )
+ })?;
+ Ok(connection)
+ }
+}
+
+impl fmt::Debug for DatabaseConnection {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("SqliteConnection")
+ .field("file_path", &self.file_path)
+ .finish()
+ }
+}
+
+pub struct DatabaseConnectionResult {
+ pub connection: anyhow::Result<SqliteConnection>,
+ pub file_path: PathBuf,
+}
+
+impl DatabaseConnectionResult {
+ pub fn ok(self) -> Result<DatabaseConnection, (PathBuf, anyhow::Error)> {
+ self.try_into()
+ }
+}
+
+impl TryFrom<DatabaseConnectionResult> for DatabaseConnection {
+ type Error = (PathBuf, anyhow::Error);
+
+ fn try_from(result: DatabaseConnectionResult) -> Result<Self, Self::Error> {
+ result
+ .connection
+ .map(|connection| DatabaseConnection {
+ connection,
+ file_path: result.file_path.clone(),
+ })
+ .map_err(|err| (result.file_path, err))
+ }
+}
diff --git a/schist_desktop_gui/src/dialog.rs b/schist_desktop_gui/src/dialog.rs
new file mode 100644
index 0000000..e2704be
--- /dev/null
+++ b/schist_desktop_gui/src/dialog.rs
@@ -0,0 +1,7 @@
+mod import_from_actualbudget_dialog;
+mod new_file_dialog;
+mod pick_file_dialog;
+
+pub use import_from_actualbudget_dialog::import_from_actualbudget_dialog;
+pub use new_file_dialog::new_file_dialog;
+pub use pick_file_dialog::pick_file_dialog;
diff --git a/schist_desktop_gui/src/dialog/import_from_actualbudget_dialog.rs b/schist_desktop_gui/src/dialog/import_from_actualbudget_dialog.rs
new file mode 100644
index 0000000..10d4ec6
--- /dev/null
+++ b/schist_desktop_gui/src/dialog/import_from_actualbudget_dialog.rs
@@ -0,0 +1,26 @@
+use actualbudget_to_schist_transformer::{
+ actualbudget_state::read_actualbudget_state, schist_state::SchistState,
+ transform_state::transform_state,
+};
+use anyhow::anyhow;
+use diesel::{Connection, SqliteConnection};
+
+pub fn import_from_actualbudget_dialog(
+) -> Option<Result<SchistState, (std::path::PathBuf, anyhow::Error)>> {
+ let path = rfd::FileDialog::new()
+ .add_filter("SQLite database", &["sqlite"])
+ .set_title("Import Actualbudget database")
+ .pick_file();
+
+ path.as_ref()
+ .map(|path| import_from_actualbudget(path).map_err(|err| (path.clone(), err)))
+}
+
+fn import_from_actualbudget(path: &std::path::PathBuf) -> anyhow::Result<SchistState> {
+ let path = path
+ .to_str()
+ .ok_or_else(|| anyhow!("Failed to stringify path: {:?}", path))?;
+ SqliteConnection::establish(path)
+ .map(|mut conn| read_actualbudget_state(&mut conn))?
+ .map(transform_state)?
+}
diff --git a/schist_desktop_gui/src/dialog/new_file_dialog.rs b/schist_desktop_gui/src/dialog/new_file_dialog.rs
new file mode 100644
index 0000000..b950f68
--- /dev/null
+++ b/schist_desktop_gui/src/dialog/new_file_dialog.rs
@@ -0,0 +1,15 @@
+use crate::{database_connection::DatabaseConnectionResult, DatabaseConnection};
+
+pub fn new_file_dialog<P>(starting_directory: Option<P>) -> Option<DatabaseConnectionResult>
+where
+ P: AsRef<std::path::Path>,
+{
+ let mut file_dialog = rfd::FileDialog::new()
+ .add_filter("SQLite database", &["sqlite"])
+ .set_file_name("schist.sqlite")
+ .set_title("New file");
+ if let Some(starting_directory) = starting_directory {
+ file_dialog = file_dialog.set_directory(starting_directory);
+ }
+ file_dialog.save_file().map(DatabaseConnection::establish)
+}
diff --git a/schist_desktop_gui/src/dialog/pick_file_dialog.rs b/schist_desktop_gui/src/dialog/pick_file_dialog.rs
new file mode 100644
index 0000000..53d36c6
--- /dev/null
+++ b/schist_desktop_gui/src/dialog/pick_file_dialog.rs
@@ -0,0 +1,14 @@
+use crate::{database_connection::DatabaseConnectionResult, DatabaseConnection};
+
+pub fn pick_file_dialog<P>(starting_directory: Option<P>) -> Option<DatabaseConnectionResult>
+where
+ P: AsRef<std::path::Path>,
+{
+ let mut file_dialog = rfd::FileDialog::new()
+ .add_filter("SQLite database", &["sqlite"])
+ .set_title("Open file");
+ if let Some(starting_directory) = starting_directory {
+ file_dialog = file_dialog.set_directory(starting_directory);
+ }
+ file_dialog.pick_file().map(DatabaseConnection::establish)
+}
diff --git a/schist_desktop_gui/src/gui.rs b/schist_desktop_gui/src/gui.rs
index 099ff74..b7405c6 100644
--- a/schist_desktop_gui/src/gui.rs
+++ b/schist_desktop_gui/src/gui.rs
@@ -8,7 +8,8 @@ use schist_queries::{accounts::get_all_accounts, buckets::get_all_buckets};
use crate::{
config::Config,
- gui::screens::{main_screen, MainScreen, Screen},
+ gui::screens::{files_screen, main_screen, FilesScreen, MainScreen, Screen},
+ paths::get_file_paths,
shortcut::KeyBind,
traits::{Component, Viewable},
};
@@ -16,6 +17,7 @@ use crate::{
#[derive(Clone, Debug)]
pub enum Message {
Event(iced::Event),
+ FilesScreenMessage(files_screen::Message),
MainScreenMessage(main_screen::Message),
RefetchedAccounts(FetchResult<Vec<Account>>),
RefetchedBuckets(FetchResult<Vec<Bucket>>),
@@ -38,38 +40,31 @@ impl FetchError {
pub struct Gui {
active_screen: Screen,
- buckets: Vec<Bucket>,
+ connection: Option<SqliteConnection>,
+ files_screen: FilesScreen,
main_screen: MainScreen,
config: Config,
}
impl Gui {
- pub fn new(connection: &mut SqliteConnection) -> (Self, iced::Task<Message>) {
- let buckets = Vec::new();
- let accounts = Vec::new();
- let mut gui = Self {
- active_screen: Screen::Main,
- buckets: buckets.clone(),
- main_screen: MainScreen::new(buckets, accounts),
+ pub fn new() -> (Self, iced::Task<Message>) {
+ let file_paths = get_file_paths();
+ let gui = Self {
+ active_screen: Screen::FilesScreen,
+ connection: None,
+ files_screen: FilesScreen::new(&file_paths),
+ main_screen: MainScreen::new(),
config: Config::default(),
};
-
- let buckets = get_all_buckets(connection).map_err(FetchError::new);
- let accounts = get_all_accounts(connection).map_err(FetchError::new);
-
- let tasks = iced::Task::batch(vec![
- gui.update(Message::RefetchedBuckets(buckets)),
- gui.update(Message::RefetchedAccounts(accounts)),
- ]);
-
- (gui, tasks)
+ (gui, iced::Task::none())
}
}
impl Gui {
pub fn view<'a>(&'a self) -> Element<'a, Message> {
match self.active_screen {
- Screen::Main => self.main_screen.view().map(Message::MainScreenMessage),
+ Screen::FilesScreen => self.files_screen.view().map(Message::FilesScreenMessage),
+ Screen::MainScreen => self.main_screen.view().map(Message::MainScreenMessage),
}
}
@@ -126,7 +121,6 @@ impl Gui {
Message::RefetchedBuckets(buckets) => {
match buckets {
Ok(buckets) => {
- self.buckets = buckets.clone();
self.main_screen
.update(main_screen::Message::SetBuckets(buckets));
}
@@ -144,6 +138,20 @@ impl Gui {
}
iced::Task::none()
}
+ Message::FilesScreenMessage(message) => match self.files_screen.update(message) {
+ 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)),
+ ])
+ }
+ },
}
}
}
diff --git a/schist_desktop_gui/src/gui/components.rs b/schist_desktop_gui/src/gui/components.rs
index fc3c838..a242b0b 100644
--- a/schist_desktop_gui/src/gui/components.rs
+++ b/schist_desktop_gui/src/gui/components.rs
@@ -9,7 +9,7 @@ pub mod transactions_view;
pub use balances_view::BalancesView;
pub use bucket_name_and_balance::BucketNameAndBalance;
pub use buckets_view::BucketsView;
-pub use button::{active_button, inactive_button};
+pub use button::{active_button, inactive_button, panel_button};
pub use navigation::Navigation;
pub use text::Text;
pub use transactions_view::TransactionsView;
diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
index e8a1e46..6a64508 100644
--- a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
+++ b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
@@ -1,6 +1,7 @@
use iced::widget::{column, row, Container};
use iced::{alignment, Element, Length};
+use crate::gui::components::Text;
use crate::gui::components::{balances_view::Message, BalancesView};
use crate::style::SPACING_LG;
use crate::traits::Viewable;
@@ -10,7 +11,7 @@ pub fn view_balances_view<'a>(balances_view: &'a BalancesView) -> Element<'a, Me
.navigation
.view()
.map(Message::NavigationMessage);
- let main_content = column![iced::widget::text(balances_view.greeting.clone())];
+ let main_content = column![Text::default(&balances_view.greeting).as_element()];
row![
Container::new(navigation).width(Length::FillPortion(1)),
Container::new(main_content).width(Length::FillPortion(3)),
diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs b/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs
index 271e76c..18ddf0b 100644
--- a/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs
+++ b/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs
@@ -6,7 +6,7 @@ use crate::gui::components::Text;
pub fn view_group_names(buckets: Vec<Bucket>) -> Vec<Text> {
buckets
.iter()
- .map(|b| b.group.clone())
+ .map(|b| b.group.as_str())
.unique()
.map(Text::new)
.collect()
@@ -96,23 +96,23 @@ mod test {
];
let group_names = view_group_names(buckets);
assert_eq!(17, group_names.len());
- assert_eq!(group_names[0], Text::new(String::from("hydrogen")));
- assert_eq!(group_names[1], Text::new(String::from("helium")));
- assert_eq!(group_names[2], Text::new(String::from("lithium")));
- assert_eq!(group_names[3], Text::new(String::from("beryllium")));
- assert_eq!(group_names[4], Text::new(String::from("boron")));
- assert_eq!(group_names[5], Text::new(String::from("carbon")));
- assert_eq!(group_names[6], Text::new(String::from("nitrogen")));
- assert_eq!(group_names[7], Text::new(String::from("oxygen")));
- assert_eq!(group_names[8], Text::new(String::from("flourine")));
- assert_eq!(group_names[9], Text::new(String::from("neon")));
- assert_eq!(group_names[10], Text::new(String::from("magnesium")));
- assert_eq!(group_names[11], Text::new(String::from("aluminium")));
- assert_eq!(group_names[12], Text::new(String::from("silicon")));
- assert_eq!(group_names[13], Text::new(String::from("phosphorus")));
- assert_eq!(group_names[14], Text::new(String::from("sulphur")));
- assert_eq!(group_names[15], Text::new(String::from("chlorine")));
- assert_eq!(group_names[16], Text::new(String::from("argon")));
+ assert_eq!(group_names[0], Text::new("hydrogen"));
+ assert_eq!(group_names[1], Text::new("helium"));
+ assert_eq!(group_names[2], Text::new("lithium"));
+ assert_eq!(group_names[3], Text::new("beryllium"));
+ assert_eq!(group_names[4], Text::new("boron"));
+ assert_eq!(group_names[5], Text::new("carbon"));
+ assert_eq!(group_names[6], Text::new("nitrogen"));
+ assert_eq!(group_names[7], Text::new("oxygen"));
+ assert_eq!(group_names[8], Text::new("flourine"));
+ assert_eq!(group_names[9], Text::new("neon"));
+ assert_eq!(group_names[10], Text::new("magnesium"));
+ assert_eq!(group_names[11], Text::new("aluminium"));
+ assert_eq!(group_names[12], Text::new("silicon"));
+ assert_eq!(group_names[13], Text::new("phosphorus"));
+ assert_eq!(group_names[14], Text::new("sulphur"));
+ assert_eq!(group_names[15], Text::new("chlorine"));
+ assert_eq!(group_names[16], Text::new("argon"));
}
#[test]
@@ -134,8 +134,8 @@ mod test {
let group_names = view_group_names(buckets);
assert_eq!(
vec![
- Text::new(String::from("hydrogen")),
- Text::new(String::from("helium")),
+ Text::new("hydrogen"),
+ Text::new("helium"),
],
group_names
);
diff --git a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs
index db3fc2e..24a3952 100644
--- a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs
+++ b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs
@@ -1,10 +1,7 @@
-use iced::{
- widget::{row, text},
- Element,
-};
+use iced::{widget::row, Element};
use schist_models::Bucket;
-use crate::gui::components::navigation;
+use crate::gui::components::{navigation, Text};
#[derive(Clone, Debug, PartialEq)]
pub struct BucketNameAndBalance {
@@ -20,8 +17,8 @@ impl BucketNameAndBalance {
impl<'a> Into<Element<'a, navigation::Message<BucketNameAndBalance>>> for BucketNameAndBalance {
fn into(self) -> Element<'a, navigation::Message<BucketNameAndBalance>> {
row![
- text(self.bucket.name.clone()),
- text(self.bucket.balance.unwrap_or(0))
+ Text::new(&self.bucket.name).as_element(),
+ Text::new(&self.bucket.balance.unwrap_or(0).to_string()).as_element(),
]
.into()
}
diff --git a/schist_desktop_gui/src/gui/components/buckets_view.rs b/schist_desktop_gui/src/gui/components/buckets_view.rs
index bb4dbb4..e513a35 100644
--- a/schist_desktop_gui/src/gui/components/buckets_view.rs
+++ b/schist_desktop_gui/src/gui/components/buckets_view.rs
@@ -6,7 +6,7 @@ use iced::{
use schist_models::Bucket;
use crate::{
- gui::components::{navigation, BucketNameAndBalance, Navigation},
+ gui::components::{navigation, BucketNameAndBalance, Navigation, Text},
style::SPACING_LG,
traits::{Component, Viewable},
};
@@ -53,7 +53,7 @@ impl<'a> Viewable<'a, Message> for BucketsView {
Message: 'a,
{
let navigation = self.navigation.view().map(Message::NavigationMessage);
- let main_content = column![iced::widget::text(self.greeting.clone())];
+ let main_content = column![Text::default(&self.greeting).as_element()];
row![
Container::new(navigation).width(Length::FillPortion(1)),
Container::new(main_content).width(Length::FillPortion(3)),
diff --git a/schist_desktop_gui/src/gui/components/button.rs b/schist_desktop_gui/src/gui/components/button.rs
index bc9e5f9..e071f33 100644
--- a/schist_desktop_gui/src/gui/components/button.rs
+++ b/schist_desktop_gui/src/gui/components/button.rs
@@ -1,5 +1,7 @@
use iced::{widget::Button, Element};
+use crate::style::*;
+
pub fn active_button<'a, Content: Into<Element<'a, Message>>, Message>(
content: Content,
) -> Button<'a, Message> {
@@ -22,3 +24,22 @@ pub fn inactive_button<'a, Content: Into<Element<'a, Message>>, Message>(
.on_press(on_press)
.width(iced::Length::Fill)
}
+
+pub fn panel_button<'a, Content: Into<Element<'a, Message>>, Message>(
+ content: Content,
+ on_press: Message,
+) -> Button<'a, Message> {
+ iced::widget::button(content)
+ .on_press(on_press)
+ .style(|theme: &iced::Theme, _status| iced::widget::button::Style {
+ background: None,
+ border: iced::Border {
+ color: theme.extended_palette().primary.base.text,
+ radius: iced::border::Radius::new(0),
+ width: 2.0,
+ },
+ ..Default::default()
+ })
+ .width(iced::Length::Fill)
+ .padding(iced::Padding::new(SPACING_MD.into()))
+}
diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs
index d334de3..90cd7bc 100644
--- a/schist_desktop_gui/src/gui/components/text.rs
+++ b/schist_desktop_gui/src/gui/components/text.rs
@@ -1,22 +1,100 @@
-use iced::Element;
-
-use crate::gui::components::navigation;
+use crate::style::*;
#[derive(Clone, Debug, PartialEq)]
pub struct Text {
pub content: String,
+ colour: Option<Colour>,
+ size: Option<Size>,
+ strength: Option<Strength>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+enum Colour {
+ Danger,
+ Primary,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+enum Strength {
+ Base,
+ Weak,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+enum Size {
+ Small,
+ Base,
}
impl Text {
- pub fn new(group_name: String) -> Self {
+ pub fn default(content: &str) -> Self {
+ Self {
+ content: String::from(content),
+ colour: Some(Colour::Primary),
+ size: Some(Size::Base),
+ strength: Some(Strength::Base),
+ }
+ }
+
+ pub fn new(content: &str) -> Self {
+ Self {
+ content: String::from(content),
+ colour: None,
+ size: None,
+ strength: None,
+ }
+ }
+
+ pub fn as_element<'a, Message>(self) -> iced::Element<'a, Message> {
+ <Self as Into<iced::Element<'a, Message>>>::into(self)
+ }
+
+ pub fn small(&self) -> Self {
+ Self {
+ size: Some(Size::Small),
+ ..self.clone()
+ }
+ }
+
+ pub fn danger(&self) -> Self {
+ Self {
+ colour: Some(Colour::Danger),
+ ..self.clone()
+ }
+ }
+
+ pub fn weak(&self) -> Self {
Self {
- content: group_name.clone(),
+ strength: Some(Strength::Weak),
+ ..self.clone()
}
}
}
-impl<'a> Into<Element<'a, navigation::Message<Text>>> for Text {
- fn into(self) -> Element<'a, navigation::Message<Text>> {
- iced::widget::text(self.content.clone()).into()
+impl<'a, Message> Into<iced::Element<'a, Message>> for Text {
+ fn into(self) -> iced::Element<'a, Message> {
+ iced::widget::text(self.content.clone())
+ .size(match self.size {
+ Some(Size::Small) => TEXT_SIZE_SM,
+ Some(Size::Base) | None => TEXT_SIZE_BASE,
+ })
+ .style(move |theme: &iced::Theme| iced::widget::text::Style {
+ color: match (&self.colour, &self.strength) {
+ (Some(Colour::Danger), Some(Strength::Base) | None) => {
+ Some(theme.extended_palette().danger.base.text)
+ }
+ (Some(Colour::Danger), Some(Strength::Weak)) => {
+ Some(theme.extended_palette().danger.weak.text)
+ }
+ (Some(Colour::Primary), Some(Strength::Base) | None) => {
+ Some(theme.extended_palette().primary.base.text)
+ }
+ (Some(Colour::Primary), Some(Strength::Weak)) => {
+ Some(theme.extended_palette().primary.weak.text)
+ }
+ (None, _) => None,
+ },
+ })
+ .into()
}
}
diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs
index 88532a9..1956d38 100644
--- a/schist_desktop_gui/src/gui/components/transactions_view.rs
+++ b/schist_desktop_gui/src/gui/components/transactions_view.rs
@@ -45,7 +45,7 @@ impl TransactionsView {
impl<'a> Viewable<'a, Message> for TransactionsView {
fn view(&'a self) -> Element<'a, Message> {
let navigation = self.navigation.view().map(Message::NavigationMessage);
- let main_content = column![iced::widget::text(self.greeting.clone())];
+ let main_content = column![Text::default(&self.greeting).as_element()];
row![
Container::new(navigation).width(Length::FillPortion(1)),
Container::new(main_content).width(Length::FillPortion(3)),
@@ -113,6 +113,6 @@ fn make_greeting(account_name: Text) -> String {
fn view_account_names(accounts: Vec<Account>) -> Vec<Text> {
accounts
.iter()
- .map(|acc| Text::new(acc.name.clone()))
+ .map(|acc| Text::new(acc.name.as_str()))
.collect()
}
diff --git a/schist_desktop_gui/src/gui/screens.rs b/schist_desktop_gui/src/gui/screens.rs
index 3ae97dd..abca2b6 100644
--- a/schist_desktop_gui/src/gui/screens.rs
+++ b/schist_desktop_gui/src/gui/screens.rs
@@ -1,9 +1,12 @@
+pub mod files_screen;
pub mod main_screen;
+pub use files_screen::FilesScreen;
pub use main_screen::MainScreen;
#[derive(Default)]
pub enum Screen {
#[default]
- Main,
+ FilesScreen,
+ MainScreen,
}
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()
}
}
diff --git a/schist_desktop_gui/src/main.rs b/schist_desktop_gui/src/main.rs
index 24aedc0..6dd2622 100644
--- a/schist_desktop_gui/src/main.rs
+++ b/schist_desktop_gui/src/main.rs
@@ -1,6 +1,8 @@
mod config;
-mod connection;
+mod database_connection;
+mod dialog;
mod gui;
+mod paths;
mod settings;
mod shortcut;
mod style;
@@ -8,31 +10,22 @@ mod theme;
mod traits;
mod window_settings;
+pub use database_connection::DatabaseConnection;
+
use anyhow::Context;
-use diesel_migrations::MigrationHarness;
-use schist_schema::migrations::MIGRATIONS;
use crate::{
- config::Config,
- connection::establish_connection,
- gui::Gui,
- settings::make_settings,
- theme::make_theme,
+ config::Config, gui::Gui, settings::make_settings, theme::make_theme,
window_settings::make_window_settings,
};
fn main() -> anyhow::Result<()> {
let config = Config::default();
- let mut connection = establish_connection()
- .context("Failed to establish database connection")?;
- connection.run_pending_migrations(MIGRATIONS)
- .expect("Failed to run pending database migrations");
-
-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(move || Gui::new(&mut connection))
- .context("Failed to run Schist GUI")
+ 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)
+ .context("Failed to run Schist GUI")
}
diff --git a/schist_desktop_gui/src/paths.rs b/schist_desktop_gui/src/paths.rs
new file mode 100644
index 0000000..f5cbf33
--- /dev/null
+++ b/schist_desktop_gui/src/paths.rs
@@ -0,0 +1,56 @@
+use std::{
+ fs::{create_dir_all, read_dir, FileType},
+ path::{Path, PathBuf},
+};
+
+use anyhow::Context;
+
+const SCHIST_DATA_DIR: &'static str = "schist";
+
+pub fn get_file_paths() -> Vec<PathBuf> {
+ get_file_paths_checked()
+ .inspect(|paths| println!("Found file paths: {:?}", paths))
+ .inspect_err(|err| println!("Couldn't find any file paths: {:?}", err))
+ .unwrap_or_else(|_| Vec::new())
+}
+
+fn get_file_paths_checked() -> anyhow::Result<Vec<PathBuf>> {
+ let schist_data_dir = get_and_make_schist_data_dir()?;
+ Ok(read_dir(schist_data_dir)?
+ .filter_map(|entry| match entry {
+ Ok(entry) => {
+ if is_sqlite_file(&entry) {
+ Some(entry.path().to_path_buf())
+ } else {
+ None
+ }
+ }
+ Err(_) => None,
+ })
+ .collect())
+}
+
+fn is_sqlite_file(entry: &std::fs::DirEntry) -> bool {
+ entry
+ .file_type()
+ .is_ok_and(|ft| (FileType::is_file(&ft) || FileType::is_symlink(&ft)))
+ && entry
+ .path()
+ .extension()
+ .is_some_and(|ext| ext.eq_ignore_ascii_case("sqlite"))
+}
+
+pub fn get_schist_database_directory() -> anyhow::Result<std::path::PathBuf> {
+ get_and_make_schist_data_dir()
+}
+
+fn get_and_make_schist_data_dir() -> anyhow::Result<PathBuf> {
+ let data_dir = dirs::data_dir().context("Failed to get app data directory")?;
+ let schist_data_dir = data_dir.join(Path::new(SCHIST_DATA_DIR));
+ create_dir_all(&schist_data_dir).context(format!(
+ "Failed to create app data directory at {}",
+ schist_data_dir.display()
+ ))?;
+ println!("Found app data directory at {:?}", schist_data_dir);
+ Ok(schist_data_dir)
+}
diff --git a/schist_desktop_gui/src/style.rs b/schist_desktop_gui/src/style.rs
index 01ec2ad..c2ecb02 100644
--- a/schist_desktop_gui/src/style.rs
+++ b/schist_desktop_gui/src/style.rs
@@ -1 +1,5 @@
+pub const SPACING_MD: u16 = 24;
pub const SPACING_LG: u16 = 32;
+
+pub const TEXT_SIZE_SM: u16 = 12;
+pub const TEXT_SIZE_BASE: u16 = 16;
diff --git a/schist_desktop_gui/src/theme.rs b/schist_desktop_gui/src/theme.rs
index 61d736b..98df36f 100644
--- a/schist_desktop_gui/src/theme.rs
+++ b/schist_desktop_gui/src/theme.rs
@@ -1,7 +1,71 @@
-use iced::Theme;
+use iced::{
+ theme::{palette, Palette},
+ Color, Theme,
+};
use crate::gui::Gui;
pub fn make_theme(_state: &Gui) -> Theme {
- Theme::GruvboxDark
+ Theme::custom_with_fn(
+ String::from("schist"),
+ Palette {
+ background: black(),
+ text: white(),
+ primary: accent_8(),
+ success: accent_8(),
+ danger: red_4(),
+ },
+ |palette| palette::Extended {
+ background: palette::Background {
+ base: palette::Pair::new(black(), palette.text),
+ weak: palette::Pair::new(grey_2(), palette.text),
+ strong: palette::Pair::new(grey_2(), palette.text),
+ },
+ primary: palette::Primary {
+ base: palette::Pair::new(palette.background, palette.text),
+ weak: palette::Pair::new(palette.background, palette.text),
+ strong: palette::Pair::new(palette.background, palette.text),
+ },
+ secondary: palette::Secondary {
+ base: palette::Pair::new(palette.background, palette.text),
+ weak: palette::Pair::new(palette.background, palette.text),
+ strong: palette::Pair::new(palette.background, palette.text),
+ },
+ success: palette::Success {
+ base: palette::Pair::new(palette.background, accent_8()),
+ weak: palette::Pair::new(palette.background, accent_8()),
+ strong: palette::Pair::new(palette.background, accent_8()),
+ },
+ danger: palette::Danger {
+ base: palette::Pair::new(palette.background, red_7()),
+ weak: palette::Pair::new(palette.background, red_7()),
+ strong: palette::Pair::new(palette.background, red_4()),
+ },
+ is_dark: true,
+ },
+ )
+}
+
+fn black() -> Color {
+ Color::from_rgb8(0, 6, 19)
+}
+
+fn grey_2() -> Color {
+ Color::from_rgb8(11, 31, 47)
+}
+
+fn white() -> Color {
+ Color::from_rgb8(225, 249, 255)
+}
+
+fn accent_8() -> Color {
+ Color::from_rgb8(129, 199, 255)
+}
+
+fn red_4() -> Color {
+ Color::from_rgb8(146, 0, 0)
+}
+
+fn red_7() -> Color {
+ Color::from_rgb8(251, 115, 98)
}