Files
outlay/outlay-gtk/src/window.rs
lashman d247c56cfa Add desktop notifications for budget threshold crossings
After saving an expense, checks if the category budget reaches 75%,
90%, or 100% thresholds. Sends gio::Notification for each newly
crossed threshold. Tracks sent notifications in database to prevent
duplicates within the same month.
2026-03-02 00:28:43 +02:00

157 lines
5.2 KiB
Rust

use adw::prelude::*;
use outlay_core::db::Database;
use std::rc::Rc;
use crate::budgets_view::BudgetsView;
use crate::charts_view::ChartsView;
use crate::history_view::HistoryView;
use crate::log_view::LogView;
pub struct MainWindow {
pub window: adw::ApplicationWindow,
pub split_view: adw::NavigationSplitView,
pub content_stack: gtk::Stack,
pub log_view: LogView,
}
struct SidebarItem {
id: &'static str,
label: &'static str,
icon: &'static str,
}
const SIDEBAR_ITEMS: &[SidebarItem] = &[
SidebarItem { id: "log", label: "Log", icon: "list-add-symbolic" },
SidebarItem { id: "history", label: "History", icon: "document-open-recent-symbolic" },
SidebarItem { id: "charts", label: "Charts", icon: "utilities-system-monitor-symbolic" },
SidebarItem { id: "budgets", label: "Budgets", icon: "wallet2-symbolic" },
SidebarItem { id: "recurring", label: "Recurring", icon: "view-refresh-symbolic" },
SidebarItem { id: "settings", label: "Settings", icon: "emblem-system-symbolic" },
];
impl MainWindow {
pub fn new(app: &adw::Application, db: Rc<Database>) -> Self {
let content_stack = gtk::Stack::new();
content_stack.set_transition_type(gtk::StackTransitionType::Crossfade);
// Log view
let log_view = LogView::new(db.clone(), app);
let log_scroll = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never)
.child(&log_view.container)
.build();
content_stack.add_named(&log_scroll, Some("log"));
// History view
let history_view = HistoryView::new(db.clone());
let history_scroll = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never)
.child(&history_view.container)
.build();
content_stack.add_named(&history_scroll, Some("history"));
// Charts view
let charts_view = ChartsView::new(db.clone());
content_stack.add_named(&charts_view.container, Some("charts"));
// Budgets view
let budgets_view = BudgetsView::new(db.clone());
content_stack.add_named(&budgets_view.container, Some("budgets"));
// Remaining pages are placeholders for now
for item in &SIDEBAR_ITEMS[4..] {
let page = adw::StatusPage::builder()
.title(item.label)
.icon_name(item.icon)
.build();
content_stack.add_named(&page, Some(item.id));
}
let sidebar_list = gtk::ListBox::new();
sidebar_list.set_selection_mode(gtk::SelectionMode::Single);
sidebar_list.add_css_class("navigation-sidebar");
for item in SIDEBAR_ITEMS {
let row = Self::make_sidebar_row(item);
sidebar_list.append(&row);
}
let content_stack_ref = content_stack.clone();
sidebar_list.connect_row_selected(move |_, row| {
if let Some(row) = row {
let idx = row.index() as usize;
if idx < SIDEBAR_ITEMS.len() {
content_stack_ref.set_visible_child_name(SIDEBAR_ITEMS[idx].id);
}
}
});
// Select the first row by default
if let Some(first_row) = sidebar_list.row_at_index(0) {
sidebar_list.select_row(Some(&first_row));
}
let sidebar_scroll = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never)
.vexpand(true)
.child(&sidebar_list)
.build();
let sidebar_toolbar = adw::ToolbarView::new();
sidebar_toolbar.add_top_bar(&adw::HeaderBar::new());
sidebar_toolbar.set_content(Some(&sidebar_scroll));
let sidebar_page = adw::NavigationPage::builder()
.title("Outlay")
.child(&sidebar_toolbar)
.build();
let content_toolbar = adw::ToolbarView::new();
content_toolbar.add_top_bar(&adw::HeaderBar::new());
content_toolbar.set_content(Some(&content_stack));
let content_page = adw::NavigationPage::builder()
.title("Outlay")
.child(&content_toolbar)
.build();
let split_view = adw::NavigationSplitView::new();
split_view.set_sidebar(Some(&sidebar_page));
split_view.set_content(Some(&content_page));
let window = adw::ApplicationWindow::builder()
.application(app)
.title("Outlay")
.default_width(900)
.default_height(600)
.content(&split_view)
.build();
MainWindow {
window,
split_view,
content_stack,
log_view,
}
}
fn make_sidebar_row(item: &SidebarItem) -> gtk::ListBoxRow {
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 12);
hbox.set_margin_top(8);
hbox.set_margin_bottom(8);
hbox.set_margin_start(12);
hbox.set_margin_end(12);
let icon = gtk::Image::from_icon_name(item.icon);
let label = gtk::Label::new(Some(item.label));
label.set_halign(gtk::Align::Start);
hbox.append(&icon);
hbox.append(&label);
let row = gtk::ListBoxRow::new();
row.set_child(Some(&hbox));
row
}
}