Files
outlay/outlay-gtk/src/wishlist_view.rs

375 lines
13 KiB
Rust

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<Database>) -> 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, &currency, &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<Database>,
active_group: &adw::PreferencesGroup,
purchased_group: &adw::PreferencesGroup,
total_label: &gtk::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, &currency, &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: &gtk::Button,
db: &Rc<Database>,
active_group: &adw::PreferencesGroup,
purchased_group: &adw::PreferencesGroup,
total_label: &gtk::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(&note_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, &currency, &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<Database>,
active_group: &adw::PreferencesGroup,
purchased_group: &adw::PreferencesGroup,
total_label: &gtk::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, &currency, &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, &currency, &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: &gtk::Widget, rows: &mut Vec<adw::ActionRow>) {
if let Some(row) = widget.downcast_ref::<adw::ActionRow>() {
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();
}
}
}