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.
157 lines
5.2 KiB
Rust
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
|
|
}
|
|
}
|