use adw::prelude::*; use outlay_core::db::Database; use std::rc::Rc; use crate::icon_theme; pub struct WishlistView { pub container: gtk::Box, } impl WishlistView { pub fn new(db: Rc) -> Self { let container = gtk::Box::new(gtk::Orientation::Vertical, 0); let toast_overlay = adw::ToastOverlay::new(); let clamp = adw::Clamp::new(); clamp.set_maximum_size(700); clamp.set_margin_start(16); clamp.set_margin_end(16); let inner = gtk::Box::new(gtk::Orientation::Vertical, 20); inner.set_margin_top(20); inner.set_margin_bottom(20); let total_card = gtk::Box::new(gtk::Orientation::Vertical, 4); total_card.add_css_class("card"); total_card.set_margin_start(4); total_card.set_margin_end(4); let total_title = gtk::Label::new(Some("WISHLIST TOTAL")); total_title.add_css_class("caption"); total_title.add_css_class("dim-label"); total_title.set_halign(gtk::Align::Start); total_title.set_margin_top(12); total_title.set_margin_start(12); let base_currency = db.get_setting("base_currency") .ok().flatten() .unwrap_or_else(|| "USD".to_string()); let total = db.get_wishlist_total().unwrap_or(0.0); let total_label = gtk::Label::new(Some(&format!("{:.2} {}", total, base_currency))); total_label.add_css_class("title-1"); total_label.set_halign(gtk::Align::Start); total_label.set_margin_start(12); total_label.set_margin_bottom(12); total_card.append(&total_title); total_card.append(&total_label); let active_group = adw::PreferencesGroup::builder() .title("WANTED") .build(); let purchased_group = adw::PreferencesGroup::builder() .title("PURCHASED") .build(); Self::load_items(&db, &active_group, &purchased_group, &total_label, &base_currency, &toast_overlay); let add_btn = gtk::Button::with_label("Add Item"); add_btn.add_css_class("suggested-action"); add_btn.add_css_class("pill"); add_btn.set_halign(gtk::Align::Center); add_btn.set_margin_top(8); { let db_ref = db.clone(); let active_ref = active_group.clone(); let purchased_ref = purchased_group.clone(); let total_ref = total_label.clone(); let currency = base_currency.clone(); let toast_ref = toast_overlay.clone(); add_btn.connect_clicked(move |btn| { Self::show_add_dialog(btn, &db_ref, &active_ref, &purchased_ref, &total_ref, ¤cy, &toast_ref); }); } inner.append(&total_card); inner.append(&active_group); inner.append(&add_btn); inner.append(&purchased_group); clamp.set_child(Some(&inner)); let scroll = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) .child(&clamp) .build(); toast_overlay.set_child(Some(&scroll)); container.append(&toast_overlay); WishlistView { container } } fn load_items( db: &Rc, active_group: &adw::PreferencesGroup, purchased_group: &adw::PreferencesGroup, total_label: >k::Label, base_currency: &str, toast_overlay: &adw::ToastOverlay, ) { Self::clear_group(active_group); Self::clear_group(purchased_group); let total = db.get_wishlist_total().unwrap_or(0.0); total_label.set_label(&format!("{:.2} {}", total, base_currency)); let active = db.list_wishlist(false).unwrap_or_default(); let purchased = db.list_wishlist(true).unwrap_or_default(); if active.is_empty() { let row = adw::ActionRow::builder() .title("No items on your wishlist") .build(); row.add_css_class("dim-label"); active_group.add(&row); } else { for item in &active { let row = adw::ActionRow::builder() .title(&item.name) .activatable(true) .build(); if let Some(note) = &item.note { if !note.is_empty() { row.set_subtitle(note); } } let priority_icon = match item.priority { 3 => Some("#e74c3c"), 2 => Some("#f39c12"), _ => None, }; if let Some(color) = priority_icon { let tinted = icon_theme::get_tinted_icon_name("tabler-alert-circle", color); let icon = gtk::Image::from_icon_name(&tinted); icon.set_pixel_size(18); row.add_prefix(&icon); } let amount_label = gtk::Label::new(Some(&format!("{:.2}", item.amount))); amount_label.add_css_class("amount-display"); row.add_suffix(&amount_label); let item_id = item.id; let db_ref = db.clone(); let active_ref = active_group.clone(); let purchased_ref = purchased_group.clone(); let total_ref = total_label.clone(); let currency = base_currency.to_string(); let toast_ref = toast_overlay.clone(); row.connect_activated(move |row| { Self::show_item_actions(row, item_id, &db_ref, &active_ref, &purchased_ref, &total_ref, ¤cy, &toast_ref); }); active_group.add(&row); } } if purchased.is_empty() { let row = adw::ActionRow::builder() .title("No purchased items") .build(); row.add_css_class("dim-label"); purchased_group.add(&row); } else { for item in purchased.iter().take(10) { let row = adw::ActionRow::builder() .title(&item.name) .subtitle(&format!("{:.2}", item.amount)) .build(); row.add_css_class("dim-label"); purchased_group.add(&row); } } } fn show_add_dialog( parent: >k::Button, db: &Rc, active_group: &adw::PreferencesGroup, purchased_group: &adw::PreferencesGroup, total_label: >k::Label, base_currency: &str, toast_overlay: &adw::ToastOverlay, ) { let dialog = adw::Dialog::builder() .title("Add Wishlist Item") .content_width(400) .content_height(350) .build(); let toolbar = adw::ToolbarView::new(); toolbar.add_top_bar(&adw::HeaderBar::new()); let content = gtk::Box::new(gtk::Orientation::Vertical, 16); content.set_margin_top(16); content.set_margin_bottom(16); content.set_margin_start(16); content.set_margin_end(16); let form = adw::PreferencesGroup::new(); let name_row = adw::EntryRow::builder() .title("Item name") .build(); form.add(&name_row); let amount_row = adw::EntryRow::builder() .title("Estimated cost") .build(); amount_row.set_input_purpose(gtk::InputPurpose::Number); crate::numpad::attach_numpad(&amount_row); form.add(&amount_row); let priority_labels = ["Low", "Medium", "High"]; let priority_model = gtk::StringList::new(&priority_labels); let priority_row = adw::ComboRow::builder() .title("Priority") .model(&priority_model) .selected(0) .build(); form.add(&priority_row); let note_row = adw::EntryRow::builder() .title("Note (optional)") .build(); form.add(¬e_row); let url_row = adw::EntryRow::builder() .title("URL (optional)") .build(); form.add(&url_row); let save_btn = gtk::Button::with_label("Save"); save_btn.add_css_class("suggested-action"); save_btn.add_css_class("pill"); save_btn.set_halign(gtk::Align::Center); content.append(&form); content.append(&save_btn); toolbar.set_content(Some(&content)); dialog.set_child(Some(&toolbar)); { let db_ref = db.clone(); let dialog_ref = dialog.clone(); let active_ref = active_group.clone(); let purchased_ref = purchased_group.clone(); let total_ref = total_label.clone(); let currency = base_currency.to_string(); let toast_ref = toast_overlay.clone(); save_btn.connect_clicked(move |_| { let name = name_row.text().to_string(); if name.trim().is_empty() { toast_ref.add_toast(adw::Toast::new("Please enter an item name")); return; } let amount: f64 = match amount_row.text().trim().parse() { Ok(v) if v > 0.0 => v, _ => { toast_ref.add_toast(adw::Toast::new("Please enter a valid amount")); return; } }; let priority = match priority_row.selected() { 2 => 3, 1 => 2, _ => 1, }; let note_text = note_row.text(); let note = if note_text.is_empty() { None } else { Some(note_text.as_str()) }; let url_text = url_row.text(); let url = if url_text.is_empty() { None } else { Some(url_text.as_str()) }; match db_ref.insert_wishlist_item(name.trim(), amount, None, url, note, priority) { Ok(_) => { dialog_ref.close(); toast_ref.add_toast(adw::Toast::new("Item added to wishlist")); Self::load_items(&db_ref, &active_ref, &purchased_ref, &total_ref, ¤cy, &toast_ref); } Err(e) => { toast_ref.add_toast(adw::Toast::new(&format!("Error: {}", e))); } } }); } dialog.present(Some(parent)); } fn show_item_actions( parent: &adw::ActionRow, item_id: i64, db: &Rc, active_group: &adw::PreferencesGroup, purchased_group: &adw::PreferencesGroup, total_label: >k::Label, base_currency: &str, toast_overlay: &adw::ToastOverlay, ) { let alert = adw::AlertDialog::new( Some("Wishlist Item"), Some("What would you like to do with this item?"), ); alert.add_response("cancel", "Cancel"); alert.add_response("purchased", "Mark as purchased"); alert.add_response("delete", "Delete"); alert.set_response_appearance("delete", adw::ResponseAppearance::Destructive); alert.set_default_response(Some("cancel")); alert.set_close_response("cancel"); let db_ref = db.clone(); let active_ref = active_group.clone(); let purchased_ref = purchased_group.clone(); let total_ref = total_label.clone(); let currency = base_currency.to_string(); let toast_ref = toast_overlay.clone(); alert.connect_response(None, move |_, response| { match response { "purchased" => { match db_ref.mark_wishlist_purchased(item_id) { Ok(()) => { toast_ref.add_toast(adw::Toast::new("Item marked as purchased")); Self::load_items(&db_ref, &active_ref, &purchased_ref, &total_ref, ¤cy, &toast_ref); } Err(e) => toast_ref.add_toast(adw::Toast::new(&format!("Error: {}", e))), } } "delete" => { match db_ref.delete_wishlist_item(item_id) { Ok(()) => { toast_ref.add_toast(adw::Toast::new("Item deleted")); Self::load_items(&db_ref, &active_ref, &purchased_ref, &total_ref, ¤cy, &toast_ref); } Err(e) => toast_ref.add_toast(adw::Toast::new(&format!("Error: {}", e))), } } _ => {} } }); alert.present(Some(parent)); } fn clear_group(group: &adw::PreferencesGroup) { let mut rows = Vec::new(); Self::collect_action_rows(group.upcast_ref(), &mut rows); for row in &rows { group.remove(row); } } fn collect_action_rows(widget: >k::Widget, rows: &mut Vec) { if let Some(row) = widget.downcast_ref::() { rows.push(row.clone()); return; } let mut child = widget.first_child(); while let Some(c) = child { Self::collect_action_rows(&c, rows); child = c.next_sibling(); } } }