Add log view UI with transaction entry form
This commit is contained in:
199
outlay-gtk/src/log_view.rs
Normal file
199
outlay-gtk/src/log_view.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use adw::prelude::*;
|
||||
use gtk::glib;
|
||||
|
||||
pub struct LogView {
|
||||
pub container: gtk::Box,
|
||||
pub type_toggle: gtk::ToggleButton,
|
||||
pub amount_row: adw::EntryRow,
|
||||
pub currency_row: adw::ComboRow,
|
||||
pub category_row: adw::ComboRow,
|
||||
pub date_label: gtk::Label,
|
||||
pub note_row: adw::EntryRow,
|
||||
pub save_button: gtk::Button,
|
||||
pub recent_group: adw::PreferencesGroup,
|
||||
}
|
||||
|
||||
impl LogView {
|
||||
pub fn new() -> Self {
|
||||
let container = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
|
||||
let clamp = adw::Clamp::new();
|
||||
clamp.set_maximum_size(600);
|
||||
clamp.set_margin_top(24);
|
||||
clamp.set_margin_bottom(24);
|
||||
clamp.set_margin_start(12);
|
||||
clamp.set_margin_end(12);
|
||||
|
||||
let inner = gtk::Box::new(gtk::Orientation::Vertical, 24);
|
||||
|
||||
// -- Transaction type toggle --
|
||||
let type_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||
type_box.add_css_class("linked");
|
||||
type_box.set_halign(gtk::Align::Center);
|
||||
|
||||
let expense_btn = gtk::ToggleButton::with_label("Expense");
|
||||
expense_btn.set_active(true);
|
||||
expense_btn.set_hexpand(true);
|
||||
|
||||
let income_btn = gtk::ToggleButton::with_label("Income");
|
||||
income_btn.set_group(Some(&expense_btn));
|
||||
income_btn.set_hexpand(true);
|
||||
|
||||
type_box.append(&expense_btn);
|
||||
type_box.append(&income_btn);
|
||||
|
||||
// -- Form group --
|
||||
let form_group = adw::PreferencesGroup::builder()
|
||||
.title("New Transaction")
|
||||
.build();
|
||||
|
||||
let amount_row = adw::EntryRow::builder()
|
||||
.title("Amount")
|
||||
.build();
|
||||
amount_row.set_input_purpose(gtk::InputPurpose::Number);
|
||||
form_group.add(&amount_row);
|
||||
|
||||
let currency_model = gtk::StringList::new(&[
|
||||
"USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "CNY", "INR", "BRL",
|
||||
]);
|
||||
let currency_row = adw::ComboRow::builder()
|
||||
.title("Currency")
|
||||
.model(¤cy_model)
|
||||
.build();
|
||||
form_group.add(¤cy_row);
|
||||
|
||||
let category_model = gtk::StringList::new(&[]);
|
||||
let category_row = adw::ComboRow::builder()
|
||||
.title("Category")
|
||||
.model(&category_model)
|
||||
.build();
|
||||
|
||||
// Populate with default expense categories
|
||||
Self::populate_categories(&category_model, true);
|
||||
|
||||
form_group.add(&category_row);
|
||||
|
||||
let today = glib::DateTime::now_local().unwrap();
|
||||
let date_str = today.format("%Y-%m-%d").unwrap().to_string();
|
||||
|
||||
let date_label = gtk::Label::new(Some(&date_str));
|
||||
date_label.set_halign(gtk::Align::End);
|
||||
date_label.set_hexpand(true);
|
||||
|
||||
let calendar = gtk::Calendar::new();
|
||||
let popover = gtk::Popover::new();
|
||||
popover.set_child(Some(&calendar));
|
||||
|
||||
let date_menu_btn = gtk::MenuButton::new();
|
||||
date_menu_btn.set_popover(Some(&popover));
|
||||
date_menu_btn.set_icon_name("x-office-calendar-symbolic");
|
||||
date_menu_btn.add_css_class("flat");
|
||||
|
||||
let date_box = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
date_box.append(&date_label);
|
||||
date_box.append(&date_menu_btn);
|
||||
|
||||
let date_row = adw::ActionRow::builder()
|
||||
.title("Date")
|
||||
.build();
|
||||
date_row.add_suffix(&date_box);
|
||||
date_row.set_activatable_widget(Some(&date_menu_btn));
|
||||
|
||||
// Connect calendar day-selected to update the label
|
||||
let date_label_ref = date_label.clone();
|
||||
let popover_ref = popover.clone();
|
||||
calendar.connect_day_selected(move |cal| {
|
||||
let dt = cal.date();
|
||||
let formatted = dt.format("%Y-%m-%d").unwrap().to_string();
|
||||
date_label_ref.set_label(&formatted);
|
||||
popover_ref.popdown();
|
||||
});
|
||||
|
||||
form_group.add(&date_row);
|
||||
|
||||
let note_row = adw::EntryRow::builder()
|
||||
.title("Note (optional)")
|
||||
.build();
|
||||
form_group.add(¬e_row);
|
||||
|
||||
// -- Save button --
|
||||
let save_button = gtk::Button::with_label("Save");
|
||||
save_button.add_css_class("suggested-action");
|
||||
save_button.add_css_class("pill");
|
||||
save_button.set_halign(gtk::Align::Center);
|
||||
save_button.set_margin_top(8);
|
||||
|
||||
// -- Wire type toggle to filter categories --
|
||||
let category_model_ref = category_model.clone();
|
||||
expense_btn.connect_toggled(move |btn| {
|
||||
if btn.is_active() {
|
||||
Self::populate_categories(&category_model_ref, true);
|
||||
}
|
||||
});
|
||||
let category_model_ref2 = category_model.clone();
|
||||
income_btn.connect_toggled(move |btn| {
|
||||
if btn.is_active() {
|
||||
Self::populate_categories(&category_model_ref2, false);
|
||||
}
|
||||
});
|
||||
|
||||
// -- Recent transactions (placeholder) --
|
||||
let recent_group = adw::PreferencesGroup::builder()
|
||||
.title("Recent")
|
||||
.build();
|
||||
|
||||
let placeholder = adw::ActionRow::builder()
|
||||
.title("No transactions yet")
|
||||
.build();
|
||||
placeholder.add_css_class("dim-label");
|
||||
recent_group.add(&placeholder);
|
||||
|
||||
// -- Assemble --
|
||||
inner.append(&type_box);
|
||||
inner.append(&form_group);
|
||||
inner.append(&save_button);
|
||||
inner.append(&recent_group);
|
||||
|
||||
clamp.set_child(Some(&inner));
|
||||
container.append(&clamp);
|
||||
|
||||
LogView {
|
||||
container,
|
||||
type_toggle: expense_btn,
|
||||
amount_row,
|
||||
currency_row,
|
||||
category_row,
|
||||
date_label,
|
||||
note_row,
|
||||
save_button,
|
||||
recent_group,
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_categories(model: >k::StringList, is_expense: bool) {
|
||||
// Remove existing items
|
||||
while model.n_items() > 0 {
|
||||
model.remove(0);
|
||||
}
|
||||
|
||||
if is_expense {
|
||||
let cats = [
|
||||
"Food & Dining", "Groceries", "Transport", "Housing",
|
||||
"Utilities", "Healthcare", "Entertainment", "Shopping",
|
||||
"Education", "Personal Care", "Travel", "Insurance",
|
||||
"Gifts & Donations", "Other Expense",
|
||||
];
|
||||
for c in cats {
|
||||
model.append(c);
|
||||
}
|
||||
} else {
|
||||
let cats = [
|
||||
"Salary", "Freelance", "Investments", "Gifts Received",
|
||||
"Refunds", "Other Income",
|
||||
];
|
||||
for c in cats {
|
||||
model.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod log_view;
|
||||
mod window;
|
||||
|
||||
use adw::prelude::*;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use adw::prelude::*;
|
||||
|
||||
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 {
|
||||
@@ -26,7 +29,16 @@ impl MainWindow {
|
||||
let content_stack = gtk::Stack::new();
|
||||
content_stack.set_transition_type(gtk::StackTransitionType::Crossfade);
|
||||
|
||||
for item in SIDEBAR_ITEMS {
|
||||
// Log view - real widget
|
||||
let log_view = LogView::new();
|
||||
let log_scroll = gtk::ScrolledWindow::builder()
|
||||
.hscrollbar_policy(gtk::PolicyType::Never)
|
||||
.child(&log_view.container)
|
||||
.build();
|
||||
content_stack.add_named(&log_scroll, Some("log"));
|
||||
|
||||
// Remaining pages are placeholders for now
|
||||
for item in &SIDEBAR_ITEMS[1..] {
|
||||
let page = adw::StatusPage::builder()
|
||||
.title(item.label)
|
||||
.icon_name(item.icon)
|
||||
@@ -98,6 +110,7 @@ impl MainWindow {
|
||||
window,
|
||||
split_view,
|
||||
content_stack,
|
||||
log_view,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user