From ed5a5e231f32429a8cbaceea60a4fa903cbac1dc Mon Sep 17 00:00:00 2001 From: lashman Date: Mon, 2 Mar 2026 00:57:05 +0200 Subject: [PATCH] Add settings view with theme, categories, export, and backup --- outlay-core/src/db.rs | 15 + outlay-gtk/Cargo.toml | 2 +- outlay-gtk/src/main.rs | 1 + outlay-gtk/src/settings_view.rs | 828 ++++++++++++++++++++++++++++++++ outlay-gtk/src/window.rs | 12 +- 5 files changed, 849 insertions(+), 9 deletions(-) create mode 100644 outlay-gtk/src/settings_view.rs diff --git a/outlay-core/src/db.rs b/outlay-core/src/db.rs index 9282897..773e614 100644 --- a/outlay-core/src/db.rs +++ b/outlay-core/src/db.rs @@ -734,6 +734,21 @@ impl Database { Ok(()) } + pub fn reset_all_data(&self) -> SqlResult<()> { + self.conn.execute_batch( + "DELETE FROM transactions; + DELETE FROM budgets; + DELETE FROM recurring_transactions; + DELETE FROM budget_notifications; + DELETE FROM categories; + DELETE FROM exchange_rate_cache; + DELETE FROM settings;" + )?; + self.seed_default_categories()?; + self.set_setting("schema_version", "1")?; + Ok(()) + } + fn seed_default_categories(&self) -> SqlResult<()> { let expense_categories = [ ("Food & Dining", "\u{1f354}", "#e74c3c"), diff --git a/outlay-gtk/Cargo.toml b/outlay-gtk/Cargo.toml index fb3cf9d..43de9cc 100644 --- a/outlay-gtk/Cargo.toml +++ b/outlay-gtk/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true [dependencies] outlay-core = { path = "../outlay-core" } -gtk = { package = "gtk4", version = "0.11" } +gtk = { package = "gtk4", version = "0.11", features = ["v4_10"] } adw = { package = "libadwaita", version = "0.9", features = ["v1_8"] } chrono = "0.4" gdk = { package = "gdk4", version = "0.11" } diff --git a/outlay-gtk/src/main.rs b/outlay-gtk/src/main.rs index a9a2902..c5a60a9 100644 --- a/outlay-gtk/src/main.rs +++ b/outlay-gtk/src/main.rs @@ -3,6 +3,7 @@ mod charts_view; mod history_view; mod log_view; mod recurring_view; +mod settings_view; mod window; use adw::prelude::*; diff --git a/outlay-gtk/src/settings_view.rs b/outlay-gtk/src/settings_view.rs new file mode 100644 index 0000000..38c1309 --- /dev/null +++ b/outlay-gtk/src/settings_view.rs @@ -0,0 +1,828 @@ +use adw::prelude::*; +use chrono::{Datelike, Local}; +use gtk::{gio, glib}; +use outlay_core::backup; +use outlay_core::db::Database; +use outlay_core::exchange::ExchangeRateService; +use outlay_core::export_csv; +use outlay_core::export_json; +use outlay_core::export_pdf; +use outlay_core::models::{NewCategory, TransactionType}; +use std::rc::Rc; + +pub struct SettingsView { + pub container: gtk::Box, +} + +impl SettingsView { + pub fn new(db: Rc, app: &adw::Application) -> Self { + let container = gtk::Box::new(gtk::Orientation::Vertical, 0); + let toast_overlay = adw::ToastOverlay::new(); + + let clamp = adw::Clamp::new(); + clamp.set_maximum_size(700); + clamp.set_margin_start(12); + clamp.set_margin_end(12); + + let inner = gtk::Box::new(gtk::Orientation::Vertical, 16); + inner.set_margin_top(16); + inner.set_margin_bottom(16); + + // Currency group + let currency_group = adw::PreferencesGroup::builder() + .title("Currency") + .build(); + + let base_currency = db + .get_setting("base_currency") + .ok() + .flatten() + .unwrap_or_else(|| "USD".to_string()); + + let currencies = ExchangeRateService::supported_currencies(); + let currency_labels: Vec = currencies + .iter() + .map(|(code, name)| format!("{} - {}", code, name)) + .collect(); + let currency_label_refs: Vec<&str> = currency_labels.iter().map(|s| s.as_str()).collect(); + let currency_model = gtk::StringList::new(¤cy_label_refs); + let currency_codes: Vec = currencies.iter().map(|(c, _)| c.to_string()).collect(); + let base_idx = currency_codes + .iter() + .position(|c| c.eq_ignore_ascii_case(&base_currency)) + .unwrap_or(0); + + let currency_row = adw::ComboRow::builder() + .title("Base Currency") + .subtitle("Used for reports and budget calculations") + .model(¤cy_model) + .selected(base_idx as u32) + .build(); + + { + let db_ref = db.clone(); + let codes = currency_codes.clone(); + let toast_ref = toast_overlay.clone(); + currency_row.connect_selected_notify(move |row| { + let idx = row.selected() as usize; + if let Some(code) = codes.get(idx) { + match db_ref.set_setting("base_currency", code) { + Ok(()) => { + let toast = adw::Toast::new(&format!("Base currency set to {}", code)); + toast_ref.add_toast(toast); + } + Err(e) => { + let toast = adw::Toast::new(&format!("Error: {}", e)); + toast_ref.add_toast(toast); + } + } + } + }); + } + + currency_group.add(¤cy_row); + + // Appearance group + let appearance_group = adw::PreferencesGroup::builder() + .title("Appearance") + .build(); + + let theme_labels = ["System", "Light", "Dark"]; + let theme_model = gtk::StringList::new(&theme_labels); + + let current_theme = db + .get_setting("theme") + .ok() + .flatten() + .unwrap_or_else(|| "system".to_string()); + let theme_idx = match current_theme.as_str() { + "light" => 1, + "dark" => 2, + _ => 0, + }; + + let theme_row = adw::ComboRow::builder() + .title("Theme") + .model(&theme_model) + .selected(theme_idx) + .build(); + + { + let db_ref = db.clone(); + theme_row.connect_selected_notify(move |row| { + let theme = match row.selected() { + 1 => "light", + 2 => "dark", + _ => "system", + }; + db_ref.set_setting("theme", theme).ok(); + Self::apply_theme(theme); + }); + } + + appearance_group.add(&theme_row); + + // Apply current theme on load + Self::apply_theme(¤t_theme); + + // Categories group + let categories_group = adw::PreferencesGroup::builder() + .title("Categories") + .build(); + + let expense_expander = adw::ExpanderRow::builder() + .title("Expense Categories") + .build(); + Self::populate_category_expander( + &db, &expense_expander, TransactionType::Expense, &toast_overlay, + ); + categories_group.add(&expense_expander); + + let income_expander = adw::ExpanderRow::builder() + .title("Income Categories") + .build(); + Self::populate_category_expander( + &db, &income_expander, TransactionType::Income, &toast_overlay, + ); + categories_group.add(&income_expander); + + let add_cat_btn = gtk::Button::with_label("Add Category"); + add_cat_btn.add_css_class("pill"); + add_cat_btn.set_halign(gtk::Align::Center); + add_cat_btn.set_margin_top(8); + + { + let db_ref = db.clone(); + let expense_ref = expense_expander.clone(); + let income_ref = income_expander.clone(); + let toast_ref = toast_overlay.clone(); + add_cat_btn.connect_clicked(move |btn| { + Self::show_add_category_dialog( + btn, &db_ref, &expense_ref, &income_ref, &toast_ref, + ); + }); + } + + // Data group + let data_group = adw::PreferencesGroup::builder() + .title("Data") + .build(); + + let export_csv_row = adw::ActionRow::builder() + .title("Export CSV") + .subtitle("Export all transactions as CSV file") + .activatable(true) + .build(); + export_csv_row.add_suffix(>k::Image::from_icon_name("document-save-symbolic")); + + let export_json_row = adw::ActionRow::builder() + .title("Export JSON") + .subtitle("Export all data as JSON file") + .activatable(true) + .build(); + export_json_row.add_suffix(>k::Image::from_icon_name("document-save-symbolic")); + + let export_pdf_row = adw::ActionRow::builder() + .title("Export PDF Report") + .subtitle("Generate a monthly PDF report") + .activatable(true) + .build(); + export_pdf_row.add_suffix(>k::Image::from_icon_name("document-save-symbolic")); + + let backup_row = adw::ActionRow::builder() + .title("Full Backup") + .subtitle("Create a backup of all data") + .activatable(true) + .build(); + backup_row.add_suffix(>k::Image::from_icon_name("document-save-symbolic")); + + let restore_row = adw::ActionRow::builder() + .title("Restore from Backup") + .subtitle("Restore data from a .outlay backup file") + .activatable(true) + .build(); + restore_row.add_suffix(>k::Image::from_icon_name("document-open-symbolic")); + + data_group.add(&export_csv_row); + data_group.add(&export_json_row); + data_group.add(&export_pdf_row); + data_group.add(&backup_row); + data_group.add(&restore_row); + + // Wire export buttons + { + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + export_csv_row.connect_activated(move |row| { + Self::export_csv_action(row, &db_ref, &toast_ref); + }); + } + { + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + export_json_row.connect_activated(move |row| { + Self::export_json_action(row, &db_ref, &toast_ref); + }); + } + { + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + export_pdf_row.connect_activated(move |row| { + Self::export_pdf_action(row, &db_ref, &toast_ref); + }); + } + { + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + backup_row.connect_activated(move |row| { + Self::backup_action(row, &db_ref, &toast_ref); + }); + } + { + let toast_ref = toast_overlay.clone(); + let app_ref = app.clone(); + restore_row.connect_activated(move |row| { + Self::restore_action(row, &toast_ref, &app_ref); + }); + } + + // Reset group + let reset_group = adw::PreferencesGroup::builder() + .title("Danger Zone") + .build(); + + let reset_row = adw::ActionRow::builder() + .title("Reset All Data") + .subtitle("Delete all transactions, budgets, and settings") + .activatable(true) + .build(); + reset_row.add_css_class("error"); + + { + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + reset_row.connect_activated(move |row| { + Self::reset_action(row, &db_ref, &toast_ref); + }); + } + + reset_group.add(&reset_row); + + inner.append(¤cy_group); + inner.append(&appearance_group); + inner.append(&categories_group); + inner.append(&add_cat_btn); + inner.append(&data_group); + inner.append(&reset_group); + + clamp.set_child(Some(&inner)); + + let scroll = gtk::ScrolledWindow::builder() + .hscrollbar_policy(gtk::PolicyType::Never) + .vexpand(true) + .child(&clamp) + .build(); + + toast_overlay.set_child(Some(&scroll)); + container.append(&toast_overlay); + + SettingsView { container } + } + + fn apply_theme(theme: &str) { + let style_manager = adw::StyleManager::default(); + match theme { + "light" => style_manager.set_color_scheme(adw::ColorScheme::ForceLight), + "dark" => style_manager.set_color_scheme(adw::ColorScheme::ForceDark), + _ => style_manager.set_color_scheme(adw::ColorScheme::Default), + } + } + + fn populate_category_expander( + db: &Rc, + expander: &adw::ExpanderRow, + txn_type: TransactionType, + toast_overlay: &adw::ToastOverlay, + ) { + // Remove existing children + while let Some(child) = expander.first_child() { + if let Some(child) = child.next_sibling() { + if let Some(row) = child.downcast_ref::() { + expander.remove(row); + } else { + break; + } + } else { + break; + } + } + + let cats = db.list_categories(Some(txn_type)).unwrap_or_default(); + for cat in &cats { + let display = match &cat.icon { + Some(icon) => format!("{} {}", icon, cat.name), + None => cat.name.clone(), + }; + + let row = adw::ActionRow::builder() + .title(&display) + .build(); + + if !cat.is_default { + let delete_btn = gtk::Button::from_icon_name("edit-delete-symbolic"); + delete_btn.add_css_class("flat"); + delete_btn.set_valign(gtk::Align::Center); + + let cat_id = cat.id; + let db_ref = db.clone(); + let expander_ref = expander.clone(); + let toast_ref = toast_overlay.clone(); + delete_btn.connect_clicked(move |btn| { + let alert = adw::AlertDialog::new( + Some("Delete this category?"), + Some("Transactions using this category will not be deleted."), + ); + alert.add_response("cancel", "Cancel"); + alert.add_response("delete", "Delete"); + alert.set_response_appearance("delete", adw::ResponseAppearance::Destructive); + alert.set_default_response(Some("cancel")); + alert.set_close_response("cancel"); + + let db_del = db_ref.clone(); + let exp_del = expander_ref.clone(); + let toast_del = toast_ref.clone(); + alert.connect_response(None, move |_, response| { + if response == "delete" { + match db_del.delete_category(cat_id) { + Ok(()) => { + let toast = adw::Toast::new("Category deleted"); + toast_del.add_toast(toast); + Self::populate_category_expander( + &db_del, &exp_del, txn_type, &toast_del, + ); + } + Err(e) => { + let toast = adw::Toast::new(&format!("Error: {}", e)); + toast_del.add_toast(toast); + } + } + } + }); + alert.present(Some(btn)); + }); + + row.add_suffix(&delete_btn); + } + + expander.add_row(&row); + } + } + + fn show_add_category_dialog( + parent: >k::Button, + db: &Rc, + expense_expander: &adw::ExpanderRow, + income_expander: &adw::ExpanderRow, + toast_overlay: &adw::ToastOverlay, + ) { + let dialog = adw::Dialog::builder() + .title("Add Category") + .content_width(360) + .content_height(350) + .build(); + + let toolbar = adw::ToolbarView::new(); + toolbar.add_top_bar(&adw::HeaderBar::new()); + + let content = gtk::Box::new(gtk::Orientation::Vertical, 16); + content.set_margin_top(16); + content.set_margin_bottom(16); + content.set_margin_start(16); + content.set_margin_end(16); + + let form = adw::PreferencesGroup::new(); + + let name_row = adw::EntryRow::builder() + .title("Name") + .build(); + form.add(&name_row); + + let icon_row = adw::EntryRow::builder() + .title("Icon (emoji)") + .build(); + form.add(&icon_row); + + let type_labels = ["Expense", "Income"]; + let type_model = gtk::StringList::new(&type_labels); + let type_row = adw::ComboRow::builder() + .title("Type") + .model(&type_model) + .build(); + form.add(&type_row); + + let save_btn = gtk::Button::with_label("Save"); + save_btn.add_css_class("suggested-action"); + save_btn.add_css_class("pill"); + save_btn.set_halign(gtk::Align::Center); + + content.append(&form); + content.append(&save_btn); + toolbar.set_content(Some(&content)); + dialog.set_child(Some(&toolbar)); + + { + let db_ref = db.clone(); + let dialog_ref = dialog.clone(); + let expense_ref = expense_expander.clone(); + let income_ref = income_expander.clone(); + let toast_ref = toast_overlay.clone(); + save_btn.connect_clicked(move |_| { + let name = name_row.text().to_string(); + if name.trim().is_empty() { + let toast = adw::Toast::new("Please enter a category name"); + toast_ref.add_toast(toast); + return; + } + + let icon_text = icon_row.text().to_string(); + let icon = if icon_text.trim().is_empty() { + None + } else { + Some(icon_text.trim().to_string()) + }; + + let txn_type = if type_row.selected() == 0 { + TransactionType::Expense + } else { + TransactionType::Income + }; + + let new_cat = NewCategory { + name: name.trim().to_string(), + icon, + color: None, + transaction_type: txn_type, + sort_order: 100, + }; + + match db_ref.insert_category(&new_cat) { + Ok(_) => { + dialog_ref.close(); + let toast = adw::Toast::new("Category added"); + toast_ref.add_toast(toast); + Self::populate_category_expander( + &db_ref, &expense_ref, TransactionType::Expense, &toast_ref, + ); + Self::populate_category_expander( + &db_ref, &income_ref, TransactionType::Income, &toast_ref, + ); + } + Err(e) => { + let toast = adw::Toast::new(&format!("Error: {}", e)); + toast_ref.add_toast(toast); + } + } + }); + } + + dialog.present(Some(parent)); + } + + fn export_csv_action( + row: &adw::ActionRow, + db: &Rc, + toast_overlay: &adw::ToastOverlay, + ) { + let filter = gtk::FileFilter::new(); + filter.add_pattern("*.csv"); + filter.set_name(Some("CSV files")); + + let filters = gio::ListStore::new::(); + filters.append(&filter); + + let dialog = gtk::FileDialog::builder() + .title("Export CSV") + .initial_name("outlay-export.csv") + .default_filter(&filter) + .filters(&filters) + .build(); + + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + let window = row.root().and_then(|r| r.downcast::().ok()); + dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result| { + if let Ok(file) = result { + if let Some(path) = file.path() { + match std::fs::File::create(&path) { + Ok(f) => { + match export_csv::export_transactions_csv(&db_ref, f, None, None) { + Ok(count) => { + let toast = adw::Toast::new(&format!( + "Exported {} transactions to CSV", + count + )); + toast_ref.add_toast(toast); + } + Err(e) => { + let toast = adw::Toast::new(&format!("Export error: {}", e)); + toast_ref.add_toast(toast); + } + } + } + Err(e) => { + let toast = adw::Toast::new(&format!("File error: {}", e)); + toast_ref.add_toast(toast); + } + } + } + } + }); + } + + fn export_json_action( + row: &adw::ActionRow, + db: &Rc, + toast_overlay: &adw::ToastOverlay, + ) { + let filter = gtk::FileFilter::new(); + filter.add_pattern("*.json"); + filter.set_name(Some("JSON files")); + + let filters = gio::ListStore::new::(); + filters.append(&filter); + + let dialog = gtk::FileDialog::builder() + .title("Export JSON") + .initial_name("outlay-export.json") + .default_filter(&filter) + .filters(&filters) + .build(); + + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + let window = row.root().and_then(|r| r.downcast::().ok()); + dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result| { + if let Ok(file) = result { + if let Some(path) = file.path() { + match std::fs::File::create(&path) { + Ok(f) => match export_json::export_json(&db_ref, f) { + Ok(data) => { + let toast = adw::Toast::new(&format!( + "Exported {} transactions to JSON", + data.transactions.len() + )); + toast_ref.add_toast(toast); + } + Err(e) => { + let toast = adw::Toast::new(&format!("Export error: {}", e)); + toast_ref.add_toast(toast); + } + }, + Err(e) => { + let toast = adw::Toast::new(&format!("File error: {}", e)); + toast_ref.add_toast(toast); + } + } + } + } + }); + } + + fn export_pdf_action( + row: &adw::ActionRow, + db: &Rc, + toast_overlay: &adw::ToastOverlay, + ) { + let today = Local::now().date_naive(); + let base_currency = db + .get_setting("base_currency") + .ok() + .flatten() + .unwrap_or_else(|| "USD".to_string()); + + let filter = gtk::FileFilter::new(); + filter.add_pattern("*.pdf"); + filter.set_name(Some("PDF files")); + + let filters = gio::ListStore::new::(); + filters.append(&filter); + + let default_name = format!( + "outlay-report-{:04}-{:02}.pdf", + today.year(), + today.month() + ); + let dialog = gtk::FileDialog::builder() + .title("Export PDF Report") + .initial_name(&default_name) + .default_filter(&filter) + .filters(&filters) + .build(); + + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + let year = today.year(); + let month = today.month(); + let window = row.root().and_then(|r| r.downcast::().ok()); + dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result| { + if let Ok(file) = result { + if let Some(path) = file.path() { + match export_pdf::generate_monthly_report( + &db_ref, + year, + month, + &base_currency, + &path, + ) { + Ok(()) => { + let toast = adw::Toast::new("PDF report exported"); + toast_ref.add_toast(toast); + } + Err(e) => { + let toast = adw::Toast::new(&format!("PDF error: {}", e)); + toast_ref.add_toast(toast); + } + } + } + } + }); + } + + fn backup_action( + row: &adw::ActionRow, + db: &Rc, + toast_overlay: &adw::ToastOverlay, + ) { + let filter = gtk::FileFilter::new(); + filter.add_pattern("*.outlay"); + filter.set_name(Some("Outlay backup files")); + + let filters = gio::ListStore::new::(); + filters.append(&filter); + + let today = Local::now().date_naive(); + let default_name = format!( + "outlay-backup-{}.outlay", + today.format("%Y-%m-%d") + ); + let dialog = gtk::FileDialog::builder() + .title("Create Backup") + .initial_name(&default_name) + .default_filter(&filter) + .filters(&filters) + .build(); + + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + let window = row.root().and_then(|r| r.downcast::().ok()); + dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result| { + if let Ok(file) = result { + if let Some(path) = file.path() { + match backup::create_backup(&db_ref, &path) { + Ok(meta) => { + let toast = adw::Toast::new(&format!( + "Backup created ({} transactions)", + meta.transaction_count + )); + toast_ref.add_toast(toast); + } + Err(e) => { + let toast = adw::Toast::new(&format!("Backup error: {}", e)); + toast_ref.add_toast(toast); + } + } + } + } + }); + } + + fn restore_action( + row: &adw::ActionRow, + toast_overlay: &adw::ToastOverlay, + app: &adw::Application, + ) { + let filter = gtk::FileFilter::new(); + filter.add_pattern("*.outlay"); + filter.set_name(Some("Outlay backup files")); + + let filters = gio::ListStore::new::(); + filters.append(&filter); + + let dialog = gtk::FileDialog::builder() + .title("Restore from Backup") + .default_filter(&filter) + .filters(&filters) + .build(); + + let toast_ref = toast_overlay.clone(); + let app_ref = app.clone(); + let window = row.root().and_then(|r| r.downcast::().ok()); + dialog.open(window.as_ref(), gio::Cancellable::NONE, move |result: Result| { + if let Ok(file) = result { + if let Some(path) = file.path() { + // Read meta first + match backup::read_backup_meta(&path) { + Ok(meta) => { + let msg = format!( + "This backup contains {} transactions and {} categories.\nCreated on {}.\n\nRestore will replace all current data.", + meta.transaction_count, + meta.category_count, + meta.export_date, + ); + let alert = adw::AlertDialog::new( + Some("Restore from Backup?"), + Some(&msg), + ); + alert.add_response("cancel", "Cancel"); + alert.add_response("restore", "Restore"); + alert.set_response_appearance( + "restore", + adw::ResponseAppearance::Destructive, + ); + alert.set_default_response(Some("cancel")); + alert.set_close_response("cancel"); + + let toast_alert = toast_ref.clone(); + let app_alert = app_ref.clone(); + let path_clone = path.to_path_buf(); + alert.connect_response(None, move |_, response| { + if response == "restore" { + let db_path = glib::user_data_dir() + .join("outlay") + .join("outlay.db"); + match backup::restore_backup(&path_clone, &db_path) { + Ok(_) => { + let toast = adw::Toast::new( + "Backup restored. Please restart the app.", + ); + toast_alert.add_toast(toast); + // Close app after short delay + let app_quit = app_alert.clone(); + glib::timeout_add_local_once( + std::time::Duration::from_secs(2), + move || { + app_quit.quit(); + }, + ); + } + Err(e) => { + let toast = adw::Toast::new(&format!( + "Restore error: {}", + e + )); + toast_alert.add_toast(toast); + } + } + } + }); + + if let Some(w) = app_ref.active_window() { + alert.present(Some(&w)); + } + } + Err(e) => { + let toast = + adw::Toast::new(&format!("Invalid backup file: {}", e)); + toast_ref.add_toast(toast); + } + } + } + } + }); + } + + fn reset_action( + row: &adw::ActionRow, + db: &Rc, + toast_overlay: &adw::ToastOverlay, + ) { + let alert = adw::AlertDialog::new( + Some("Reset All Data?"), + Some("This will permanently delete all transactions, budgets, recurring transactions, and settings. This cannot be undone."), + ); + alert.add_response("cancel", "Cancel"); + alert.add_response("reset", "Reset Everything"); + alert.set_response_appearance("reset", adw::ResponseAppearance::Destructive); + alert.set_default_response(Some("cancel")); + alert.set_close_response("cancel"); + + let db_ref = db.clone(); + let toast_ref = toast_overlay.clone(); + alert.connect_response(None, move |_, response| { + if response == "reset" { + match db_ref.reset_all_data() { + Ok(()) => { + let toast = adw::Toast::new("All data has been reset"); + toast_ref.add_toast(toast); + } + Err(e) => { + let toast = adw::Toast::new(&format!("Error: {}", e)); + toast_ref.add_toast(toast); + } + } + } + }); + + alert.present(Some(row)); + } +} diff --git a/outlay-gtk/src/window.rs b/outlay-gtk/src/window.rs index b3a1b2f..47c2b68 100644 --- a/outlay-gtk/src/window.rs +++ b/outlay-gtk/src/window.rs @@ -7,6 +7,7 @@ use crate::charts_view::ChartsView; use crate::history_view::HistoryView; use crate::log_view::LogView; use crate::recurring_view::RecurringView; +use crate::settings_view::SettingsView; pub struct MainWindow { pub window: adw::ApplicationWindow, @@ -63,14 +64,9 @@ impl MainWindow { let recurring_view = RecurringView::new(db.clone()); content_stack.add_named(&recurring_view.container, Some("recurring")); - // Settings placeholder - for item in &SIDEBAR_ITEMS[5..] { - let page = adw::StatusPage::builder() - .title(item.label) - .icon_name(item.icon) - .build(); - content_stack.add_named(&page, Some(item.id)); - } + // Settings view + let settings_view = SettingsView::new(db.clone(), app); + content_stack.add_named(&settings_view.container, Some("settings")); let sidebar_list = gtk::ListBox::new(); sidebar_list.set_selection_mode(gtk::SelectionMode::Single);