Add settings view with theme, categories, export, and backup

This commit is contained in:
2026-03-02 00:57:05 +02:00
parent 2fff781a53
commit ed5a5e231f
5 changed files with 849 additions and 9 deletions

View File

@@ -734,6 +734,21 @@ impl Database {
Ok(()) 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<()> { fn seed_default_categories(&self) -> SqlResult<()> {
let expense_categories = [ let expense_categories = [
("Food & Dining", "\u{1f354}", "#e74c3c"), ("Food & Dining", "\u{1f354}", "#e74c3c"),

View File

@@ -5,7 +5,7 @@ edition.workspace = true
[dependencies] [dependencies]
outlay-core = { path = "../outlay-core" } 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"] } adw = { package = "libadwaita", version = "0.9", features = ["v1_8"] }
chrono = "0.4" chrono = "0.4"
gdk = { package = "gdk4", version = "0.11" } gdk = { package = "gdk4", version = "0.11" }

View File

@@ -3,6 +3,7 @@ mod charts_view;
mod history_view; mod history_view;
mod log_view; mod log_view;
mod recurring_view; mod recurring_view;
mod settings_view;
mod window; mod window;
use adw::prelude::*; use adw::prelude::*;

View File

@@ -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<Database>, 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<String> = 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(&currency_label_refs);
let currency_codes: Vec<String> = 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(&currency_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(&currency_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(&current_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(&gtk::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(&gtk::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(&gtk::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(&gtk::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(&gtk::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(&currency_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<Database>,
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::<adw::ActionRow>() {
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: &gtk::Button,
db: &Rc<Database>,
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<Database>,
toast_overlay: &adw::ToastOverlay,
) {
let filter = gtk::FileFilter::new();
filter.add_pattern("*.csv");
filter.set_name(Some("CSV files"));
let filters = gio::ListStore::new::<gtk::FileFilter>();
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::<gtk::Window>().ok());
dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
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<Database>,
toast_overlay: &adw::ToastOverlay,
) {
let filter = gtk::FileFilter::new();
filter.add_pattern("*.json");
filter.set_name(Some("JSON files"));
let filters = gio::ListStore::new::<gtk::FileFilter>();
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::<gtk::Window>().ok());
dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
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<Database>,
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::<gtk::FileFilter>();
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::<gtk::Window>().ok());
dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
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<Database>,
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::<gtk::FileFilter>();
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::<gtk::Window>().ok());
dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
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::<gtk::FileFilter>();
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::<gtk::Window>().ok());
dialog.open(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
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<Database>,
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));
}
}

View File

@@ -7,6 +7,7 @@ use crate::charts_view::ChartsView;
use crate::history_view::HistoryView; use crate::history_view::HistoryView;
use crate::log_view::LogView; use crate::log_view::LogView;
use crate::recurring_view::RecurringView; use crate::recurring_view::RecurringView;
use crate::settings_view::SettingsView;
pub struct MainWindow { pub struct MainWindow {
pub window: adw::ApplicationWindow, pub window: adw::ApplicationWindow,
@@ -63,14 +64,9 @@ impl MainWindow {
let recurring_view = RecurringView::new(db.clone()); let recurring_view = RecurringView::new(db.clone());
content_stack.add_named(&recurring_view.container, Some("recurring")); content_stack.add_named(&recurring_view.container, Some("recurring"));
// Settings placeholder // Settings view
for item in &SIDEBAR_ITEMS[5..] { let settings_view = SettingsView::new(db.clone(), app);
let page = adw::StatusPage::builder() content_stack.add_named(&settings_view.container, Some("settings"));
.title(item.label)
.icon_name(item.icon)
.build();
content_stack.add_named(&page, Some(item.id));
}
let sidebar_list = gtk::ListBox::new(); let sidebar_list = gtk::ListBox::new();
sidebar_list.set_selection_mode(gtk::SelectionMode::Single); sidebar_list.set_selection_mode(gtk::SelectionMode::Single);