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;
|
mod window;
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
|
||||||
|
use crate::log_view::LogView;
|
||||||
|
|
||||||
pub struct MainWindow {
|
pub struct MainWindow {
|
||||||
pub window: adw::ApplicationWindow,
|
pub window: adw::ApplicationWindow,
|
||||||
pub split_view: adw::NavigationSplitView,
|
pub split_view: adw::NavigationSplitView,
|
||||||
pub content_stack: gtk::Stack,
|
pub content_stack: gtk::Stack,
|
||||||
|
pub log_view: LogView,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SidebarItem {
|
struct SidebarItem {
|
||||||
@@ -26,7 +29,16 @@ impl MainWindow {
|
|||||||
let content_stack = gtk::Stack::new();
|
let content_stack = gtk::Stack::new();
|
||||||
content_stack.set_transition_type(gtk::StackTransitionType::Crossfade);
|
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()
|
let page = adw::StatusPage::builder()
|
||||||
.title(item.label)
|
.title(item.label)
|
||||||
.icon_name(item.icon)
|
.icon_name(item.icon)
|
||||||
@@ -98,6 +110,7 @@ impl MainWindow {
|
|||||||
window,
|
window,
|
||||||
split_view,
|
split_view,
|
||||||
content_stack,
|
content_stack,
|
||||||
|
log_view,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user