375 lines
13 KiB
Rust
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, ¤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<Database>,
|
|
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<Database>,
|
|
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<Database>,
|
|
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<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();
|
|
}
|
|
}
|
|
}
|