Add log view UI with transaction entry form
Expense/Income toggle, amount entry, currency dropdown, category dropdown (filtered by transaction type), date picker with calendar popover, optional note field, save button, and recent transactions placeholder section.
This commit is contained in:
204
outlay-gtk/src/log_view.rs
Normal file
204
outlay-gtk/src/log_view.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
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();
|
||||
|
||||
// Amount
|
||||
let amount_row = adw::EntryRow::builder()
|
||||
.title("Amount")
|
||||
.build();
|
||||
amount_row.set_input_purpose(gtk::InputPurpose::Number);
|
||||
form_group.add(&amount_row);
|
||||
|
||||
// Currency
|
||||
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);
|
||||
|
||||
// Category
|
||||
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);
|
||||
|
||||
// Date picker
|
||||
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);
|
||||
|
||||
// Note
|
||||
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