use adw::prelude::*; use chrono::NaiveDate; use outlay_core::db::Database; use outlay_core::exchange::ExchangeRateService; use outlay_core::models::{Frequency, NewSubscription, Subscription}; use std::cell::RefCell; use std::rc::Rc; use crate::icon_theme; type ChangeCb = Rc>>>; pub struct SubscriptionsView { pub container: gtk::Box, db: Rc, active_group: adw::PreferencesGroup, paused_group: adw::PreferencesGroup, monthly_label: gtk::Label, yearly_label: gtk::Label, base_currency: String, toast_overlay: adw::ToastOverlay, on_change: ChangeCb, } impl SubscriptionsView { 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 base_currency = db.get_setting("base_currency") .ok().flatten() .unwrap_or_else(|| "USD".to_string()); // Cost summary card let cost_card = gtk::Box::new(gtk::Orientation::Vertical, 4); cost_card.add_css_class("card"); cost_card.set_margin_start(4); cost_card.set_margin_end(4); let cost_title = gtk::Label::new(Some("SUBSCRIPTION COST")); cost_title.add_css_class("caption"); cost_title.add_css_class("dim-label"); cost_title.set_halign(gtk::Align::Start); cost_title.set_margin_top(12); cost_title.set_margin_start(12); let monthly = db.get_subscription_monthly_total().unwrap_or(0.0); let yearly = db.get_subscription_yearly_total().unwrap_or(0.0); let monthly_label = gtk::Label::new(Some(&format!("{:.2} {}/month", monthly, base_currency))); monthly_label.add_css_class("title-1"); monthly_label.set_halign(gtk::Align::Start); monthly_label.set_margin_start(12); let yearly_label = gtk::Label::new(Some(&format!("{:.2} {}/year", yearly, base_currency))); yearly_label.add_css_class("caption"); yearly_label.add_css_class("dim-label"); yearly_label.set_halign(gtk::Align::Start); yearly_label.set_margin_start(12); yearly_label.set_margin_bottom(12); cost_card.append(&cost_title); cost_card.append(&monthly_label); cost_card.append(&yearly_label); // Active subscriptions group let active_group = adw::PreferencesGroup::builder() .title("ACTIVE SUBSCRIPTIONS") .build(); // Paused subscriptions group let paused_group = adw::PreferencesGroup::builder() .title("PAUSED SUBSCRIPTIONS") .build(); let on_change: ChangeCb = Rc::new(RefCell::new(None)); Self::load_subscriptions( &db, &active_group, &paused_group, &monthly_label, &yearly_label, &base_currency, &toast_overlay, &on_change, ); // Add button let add_btn = gtk::Button::with_label("Add Subscription"); add_btn.add_css_class("suggested-action"); add_btn.add_css_class("pill"); add_btn.set_halign(gtk::Align::Center); { let db_ref = db.clone(); let active_ref = active_group.clone(); let paused_ref = paused_group.clone(); let monthly_ref = monthly_label.clone(); let yearly_ref = yearly_label.clone(); let base_ref = base_currency.clone(); let toast_ref = toast_overlay.clone(); let change_ref = on_change.clone(); add_btn.connect_clicked(move |btn| { Self::show_add_dialog( btn, &db_ref, &active_ref, &paused_ref, &monthly_ref, &yearly_ref, &base_ref, &toast_ref, &change_ref, ); }); } inner.append(&cost_card); inner.append(&active_group); inner.append(&paused_group); inner.append(&add_btn); 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); SubscriptionsView { container, db, active_group, paused_group, monthly_label, yearly_label, base_currency, toast_overlay, on_change, } } pub fn refresh(&self) { Self::load_subscriptions( &self.db, &self.active_group, &self.paused_group, &self.monthly_label, &self.yearly_label, &self.base_currency, &self.toast_overlay, &self.on_change, ); } pub fn set_on_change(&self, cb: impl Fn() + 'static) { *self.on_change.borrow_mut() = Some(Box::new(cb)); } fn notify_change(on_change: &ChangeCb) { if let Some(cb) = on_change.borrow().as_ref() { cb(); } } fn load_subscriptions( db: &Rc, active_group: &adw::PreferencesGroup, paused_group: &adw::PreferencesGroup, monthly_label: >k::Label, yearly_label: >k::Label, base_currency: &str, toast_overlay: &adw::ToastOverlay, on_change: &ChangeCb, ) { Self::clear_group(active_group); Self::clear_group(paused_group); let monthly = db.get_subscription_monthly_total().unwrap_or(0.0); let yearly = db.get_subscription_yearly_total().unwrap_or(0.0); monthly_label.set_label(&format!("{:.2} {}/month", monthly, base_currency)); yearly_label.set_label(&format!("{:.2} {}/year", yearly, base_currency)); let subs = db.list_subscriptions_v2().unwrap_or_default(); let active_subs: Vec<&Subscription> = subs.iter().filter(|s| s.active).collect(); let paused_subs: Vec<&Subscription> = subs.iter().filter(|s| !s.active).collect(); if active_subs.is_empty() { let row = adw::ActionRow::builder() .title("No active subscriptions") .subtitle("Add your first subscription") .build(); row.add_css_class("dim-label"); active_group.add(&row); } else { for sub in &active_subs { let row = Self::make_subscription_row( db, sub, active_group, paused_group, monthly_label, yearly_label, base_currency, toast_overlay, on_change, ); active_group.add(&row); } } if paused_subs.is_empty() { paused_group.set_visible(false); } else { paused_group.set_visible(true); for sub in &paused_subs { let row = Self::make_subscription_row( db, sub, active_group, paused_group, monthly_label, yearly_label, base_currency, toast_overlay, on_change, ); paused_group.add(&row); } } } fn make_subscription_row( db: &Rc, sub: &Subscription, active_group: &adw::PreferencesGroup, paused_group: &adw::PreferencesGroup, monthly_label: >k::Label, yearly_label: >k::Label, base_currency: &str, toast_overlay: &adw::ToastOverlay, on_change: &ChangeCb, ) -> adw::ActionRow { let cat = db.get_subscription_category(sub.category_id).ok(); let cat_icon = cat.as_ref().and_then(|c| c.icon.clone()); let cat_color = cat.as_ref().and_then(|c| c.color.clone()); let freq_label = match sub.frequency { Frequency::Daily => "Daily", Frequency::Weekly => "Weekly", Frequency::Biweekly => "Biweekly", Frequency::Monthly => "Monthly", Frequency::Yearly => "Yearly", }; let row = adw::ActionRow::builder() .title(&sub.name) .subtitle(freq_label) .build(); let icon_name = icon_theme::resolve_category_icon(&cat_icon, &cat_color); if let Some(name) = &icon_name { let icon = gtk::Image::from_icon_name(name); icon.set_pixel_size(24); row.add_prefix(&icon); } let suffix_box = gtk::Box::new(gtk::Orientation::Horizontal, 4); suffix_box.set_valign(gtk::Align::Center); let monthly_equiv = match sub.frequency { Frequency::Daily => sub.amount * 30.0, Frequency::Weekly => sub.amount * 4.33, Frequency::Biweekly => sub.amount * 2.17, Frequency::Monthly => sub.amount, Frequency::Yearly => sub.amount / 12.0, }; let equiv_label = gtk::Label::new(Some(&format!("{:.2} {}/mo", monthly_equiv, sub.currency))); equiv_label.add_css_class("caption"); equiv_label.add_css_class("dim-label"); suffix_box.append(&equiv_label); // Pause/resume button let pause_btn = if sub.active { let btn = gtk::Button::from_icon_name("tabler-player-pause"); btn.set_tooltip_text(Some("Pause")); btn } else { let btn = gtk::Button::from_icon_name("tabler-player-play"); btn.set_tooltip_text(Some("Resume")); btn }; pause_btn.add_css_class("flat"); { let sub_id = sub.id; let db_ref = db.clone(); let active_ref = active_group.clone(); let paused_ref = paused_group.clone(); let monthly_ref = monthly_label.clone(); let yearly_ref = yearly_label.clone(); let base_ref = base_currency.to_string(); let toast_ref = toast_overlay.clone(); let change_ref = on_change.clone(); pause_btn.connect_clicked(move |_| { let _ = db_ref.toggle_subscription_active(sub_id); Self::load_subscriptions( &db_ref, &active_ref, &paused_ref, &monthly_ref, &yearly_ref, &base_ref, &toast_ref, &change_ref, ); Self::notify_change(&change_ref); }); } suffix_box.append(&pause_btn); // Edit button let edit_btn = gtk::Button::from_icon_name("tabler-edit"); edit_btn.add_css_class("flat"); edit_btn.set_tooltip_text(Some("Edit")); { let sub_clone = sub.clone(); let db_ref = db.clone(); let active_ref = active_group.clone(); let paused_ref = paused_group.clone(); let monthly_ref = monthly_label.clone(); let yearly_ref = yearly_label.clone(); let base_ref = base_currency.to_string(); let toast_ref = toast_overlay.clone(); let change_ref = on_change.clone(); edit_btn.connect_clicked(move |btn| { Self::show_edit_dialog( btn, &db_ref, &sub_clone, &active_ref, &paused_ref, &monthly_ref, &yearly_ref, &base_ref, &toast_ref, &change_ref, ); }); } suffix_box.append(&edit_btn); // Delete button let del_btn = gtk::Button::from_icon_name("tabler-trash"); del_btn.add_css_class("flat"); del_btn.set_tooltip_text(Some("Delete")); { let sub_id = sub.id; let db_ref = db.clone(); let active_ref = active_group.clone(); let paused_ref = paused_group.clone(); let monthly_ref = monthly_label.clone(); let yearly_ref = yearly_label.clone(); let base_ref = base_currency.to_string(); let toast_ref = toast_overlay.clone(); let change_ref = on_change.clone(); del_btn.connect_clicked(move |_| { let _ = db_ref.delete_subscription_with_cascade(sub_id); Self::load_subscriptions( &db_ref, &active_ref, &paused_ref, &monthly_ref, &yearly_ref, &base_ref, &toast_ref, &change_ref, ); Self::notify_change(&change_ref); toast_ref.add_toast(adw::Toast::new("Subscription deleted")); }); } suffix_box.append(&del_btn); row.add_suffix(&suffix_box); row } fn show_add_dialog( parent: >k::Button, db: &Rc, active_group: &adw::PreferencesGroup, paused_group: &adw::PreferencesGroup, monthly_label: >k::Label, yearly_label: >k::Label, base_currency: &str, toast_overlay: &adw::ToastOverlay, on_change: &ChangeCb, ) { let dialog = adw::Dialog::builder() .title("Add Subscription") .content_width(400) .content_height(500) .build(); let toolbar = adw::ToolbarView::new(); toolbar.add_top_bar(&adw::HeaderBar::new()); // Wizard stack with two steps let wizard = gtk::Stack::new(); wizard.set_transition_type(gtk::StackTransitionType::SlideLeftRight); // --- Step 1: Essentials --- let step1 = gtk::Box::new(gtk::Orientation::Vertical, 16); step1.set_margin_top(16); step1.set_margin_bottom(16); step1.set_margin_start(16); step1.set_margin_end(16); let form1 = adw::PreferencesGroup::new(); // Name let name_row = adw::EntryRow::builder() .title("Name") .build(); form1.add(&name_row); // Amount let amount_row = adw::EntryRow::builder() .title("Amount") .build(); amount_row.set_input_purpose(gtk::InputPurpose::Number); crate::numpad::attach_numpad(&amount_row); form1.add(&amount_row); // Category let cat_model = gtk::StringList::new(&[]); let cat_ids: Rc>> = Rc::new(RefCell::new(Vec::new())); if let Ok(cats) = db.list_subscription_categories() { for cat in &cats { let icon_name = icon_theme::resolve_category_icon(&cat.icon, &cat.color); let entry = match &icon_name { Some(icon) => format!("{}\t{}", icon, cat.name), None => cat.name.clone(), }; cat_model.append(&entry); cat_ids.borrow_mut().push(cat.id); } } let cat_row = adw::ComboRow::builder() .title("Category") .model(&cat_model) .build(); cat_row.set_factory(Some(&Self::make_category_factory())); cat_row.set_list_factory(Some(&Self::make_category_factory())); form1.add(&cat_row); // Frequency let freq_labels = ["Daily", "Weekly", "Biweekly", "Monthly", "Yearly"]; let freq_model = gtk::StringList::new(&freq_labels); let freq_row = adw::ComboRow::builder() .title("Frequency") .model(&freq_model) .selected(3u32) // Monthly default .build(); form1.add(&freq_row); let step_label = gtk::Label::new(Some("Step 1 of 2")); step_label.add_css_class("caption"); step_label.add_css_class("dim-label"); let next_btn = gtk::Button::with_label("Next"); next_btn.add_css_class("suggested-action"); next_btn.add_css_class("pill"); next_btn.set_halign(gtk::Align::Center); step1.append(&form1); step1.append(&step_label); step1.append(&next_btn); // --- Step 2: Details --- let step2 = gtk::Box::new(gtk::Orientation::Vertical, 16); step2.set_margin_top(16); step2.set_margin_bottom(16); step2.set_margin_start(16); step2.set_margin_end(16); let form2 = adw::PreferencesGroup::new(); // Currency let base_cur = db.get_setting("base_currency") .ok().flatten() .unwrap_or_else(|| "USD".to_string()); let mut currencies = ExchangeRateService::supported_currencies(); if let Some(pos) = currencies.iter().position(|(c, _)| c.eq_ignore_ascii_case(&base_cur)) { let base = currencies.remove(pos); currencies.insert(0, base); } let currency_labels: Vec = currencies .iter() .map(|(code, name)| format!("{} - {}", code, name)) .collect(); let currency_label_refs: Vec<&str> = currency_labels.iter().map(|s| s.as_str()).collect(); let currency_model = gtk::StringList::new(¤cy_label_refs); let currency_codes: Vec = currencies.iter().map(|(c, _)| c.to_string()).collect(); let currency_row = adw::ComboRow::builder() .title("Currency") .model(¤cy_model) .selected(0u32) .build(); form2.add(¤cy_row); // Start date let today_str = chrono::Local::now().date_naive().format("%Y-%m-%d").to_string(); let (start_row, start_date_label) = crate::date_picker::make_date_row("Start date", &today_str); form2.add(&start_row); // Note (optional) let note_row = adw::EntryRow::builder() .title("Note (optional)") .build(); form2.add(¬e_row); // URL (optional) let url_row = adw::EntryRow::builder() .title("URL (optional)") .build(); form2.add(&url_row); let step2_label = gtk::Label::new(Some("Step 2 of 2")); step2_label.add_css_class("caption"); step2_label.add_css_class("dim-label"); let btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 12); btn_box.set_halign(gtk::Align::Center); let back_btn = gtk::Button::with_label("Back"); back_btn.add_css_class("pill"); let save_btn = gtk::Button::with_label("Save"); save_btn.add_css_class("suggested-action"); save_btn.add_css_class("pill"); btn_box.append(&back_btn); btn_box.append(&save_btn); step2.append(&form2); step2.append(&step2_label); step2.append(&btn_box); // Wire wizard navigation { let w = wizard.clone(); let name_ref = name_row.clone(); let amount_ref = amount_row.clone(); let ids_ref = cat_ids.clone(); let cat_ref = cat_row.clone(); let toast_ref = toast_overlay.clone(); next_btn.connect_clicked(move |_| { if name_ref.text().trim().is_empty() { toast_ref.add_toast(adw::Toast::new("Please enter a name")); return; } let _: f64 = match amount_ref.text().trim().parse() { Ok(v) if v > 0.0 => v, _ => { toast_ref.add_toast(adw::Toast::new("Please enter a valid amount")); return; } }; let cat_idx = cat_ref.selected() as usize; if ids_ref.borrow().get(cat_idx).is_none() { toast_ref.add_toast(adw::Toast::new("Please select a category")); return; } w.set_visible_child_name("step2"); }); } { let w = wizard.clone(); back_btn.connect_clicked(move |_| { w.set_visible_child_name("step1"); }); } wizard.add_named(&step1, Some("step1")); wizard.add_named(&step2, Some("step2")); let scroll = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) .child(&wizard) .build(); toolbar.set_content(Some(&scroll)); dialog.set_child(Some(&toolbar)); { let db_ref = db.clone(); let dialog_ref = dialog.clone(); let active_ref = active_group.clone(); let paused_ref = paused_group.clone(); let monthly_ref = monthly_label.clone(); let yearly_ref = yearly_label.clone(); let base_ref = base_currency.to_string(); let toast_ref = toast_overlay.clone(); let cat_ids = cat_ids.clone(); let currency_codes = currency_codes.clone(); let change_ref = on_change.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 a name")); return; } let amount_text = amount_row.text().to_string(); let amount: f64 = match amount_text.parse() { Ok(v) if v > 0.0 => v, _ => { toast_ref.add_toast(adw::Toast::new("Please enter a valid amount")); return; } }; let currency = currency_codes .get(currency_row.selected() as usize) .cloned() .unwrap_or_else(|| "USD".to_string()); let frequency = match freq_row.selected() { 0 => Frequency::Daily, 1 => Frequency::Weekly, 2 => Frequency::Biweekly, 3 => Frequency::Monthly, _ => Frequency::Yearly, }; let cat_idx = cat_row.selected() as usize; let ids = cat_ids.borrow(); let category_id = match ids.get(cat_idx) { Some(&id) => id, None => { toast_ref.add_toast(adw::Toast::new("Please select a category")); return; } }; let start_str = start_date_label.text().to_string(); let start_date = NaiveDate::parse_from_str(&start_str, "%Y-%m-%d") .unwrap_or_else(|_| chrono::Local::now().date_naive()); let note_text = note_row.text(); let note = if note_text.is_empty() { None } else { Some(note_text.to_string()) }; let url_text = url_row.text(); let url = if url_text.is_empty() { None } else { Some(url_text.to_string()) }; let new_sub = NewSubscription { name: name.trim().to_string(), amount, currency, frequency, category_id, start_date, note, url, recurring_id: None, }; let sub_cat_id = db_ref.find_subscriptions_category_id() .ok().flatten() .unwrap_or(category_id); match db_ref.insert_linked_subscription_and_recurring(&new_sub, sub_cat_id) { Ok(_) => { dialog_ref.close(); Self::load_subscriptions( &db_ref, &active_ref, &paused_ref, &monthly_ref, &yearly_ref, &base_ref, &toast_ref, &change_ref, ); Self::notify_change(&change_ref); toast_ref.add_toast(adw::Toast::new("Subscription added")); } Err(e) => { toast_ref.add_toast(adw::Toast::new(&format!("Error: {}", e))); } } }); } dialog.present(Some(parent)); } fn show_edit_dialog( parent: >k::Button, db: &Rc, sub: &Subscription, active_group: &adw::PreferencesGroup, paused_group: &adw::PreferencesGroup, monthly_label: >k::Label, yearly_label: >k::Label, base_currency: &str, toast_overlay: &adw::ToastOverlay, on_change: &ChangeCb, ) { let dialog = adw::Dialog::builder() .title("Edit Subscription") .content_width(400) .content_height(500) .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(); // Name let name_row = adw::EntryRow::builder() .title("Name") .text(&sub.name) .build(); form.add(&name_row); // Amount let amount_row = adw::EntryRow::builder() .title("Amount") .text(&format!("{:.2}", sub.amount)) .build(); amount_row.set_input_purpose(gtk::InputPurpose::Number); crate::numpad::attach_numpad(&amount_row); form.add(&amount_row); // Currency let mut currencies = ExchangeRateService::supported_currencies(); let base_cur = db.get_setting("base_currency") .ok().flatten() .unwrap_or_else(|| "USD".to_string()); if let Some(pos) = currencies.iter().position(|(c, _)| c.eq_ignore_ascii_case(&base_cur)) { let base = currencies.remove(pos); currencies.insert(0, base); } let currency_labels: Vec = currencies .iter() .map(|(code, name)| format!("{} - {}", code, name)) .collect(); let currency_label_refs: Vec<&str> = currency_labels.iter().map(|s| s.as_str()).collect(); let currency_model = gtk::StringList::new(¤cy_label_refs); let currency_codes: Vec = currencies.iter().map(|(c, _)| c.to_string()).collect(); let selected_currency = currency_codes.iter().position(|c| c == &sub.currency).unwrap_or(0) as u32; let currency_row = adw::ComboRow::builder() .title("Currency") .model(¤cy_model) .selected(selected_currency) .build(); form.add(¤cy_row); // Frequency let freq_labels = ["Daily", "Weekly", "Biweekly", "Monthly", "Yearly"]; let freq_model = gtk::StringList::new(&freq_labels); let selected_freq = match sub.frequency { Frequency::Daily => 0u32, Frequency::Weekly => 1, Frequency::Biweekly => 2, Frequency::Monthly => 3, Frequency::Yearly => 4, }; let freq_row = adw::ComboRow::builder() .title("Frequency") .model(&freq_model) .selected(selected_freq) .build(); form.add(&freq_row); // Category let cat_model = gtk::StringList::new(&[]); let cat_ids: Rc>> = Rc::new(RefCell::new(Vec::new())); let mut selected_cat = 0u32; if let Ok(cats) = db.list_subscription_categories() { for (i, cat) in cats.iter().enumerate() { let icon_name = icon_theme::resolve_category_icon(&cat.icon, &cat.color); let entry = match &icon_name { Some(icon) => format!("{}\t{}", icon, cat.name), None => cat.name.clone(), }; cat_model.append(&entry); cat_ids.borrow_mut().push(cat.id); if cat.id == sub.category_id { selected_cat = i as u32; } } } let cat_row = adw::ComboRow::builder() .title("Category") .model(&cat_model) .selected(selected_cat) .build(); cat_row.set_factory(Some(&Self::make_category_factory())); cat_row.set_list_factory(Some(&Self::make_category_factory())); form.add(&cat_row); // Start date let start_str = sub.start_date.format("%Y-%m-%d").to_string(); let (start_row, start_date_label) = crate::date_picker::make_date_row("Start date", &start_str); form.add(&start_row); // Note (optional) let note_row = adw::EntryRow::builder() .title("Note (optional)") .text(sub.note.as_deref().unwrap_or("")) .build(); form.add(¬e_row); // URL (optional) let url_row = adw::EntryRow::builder() .title("URL (optional)") .text(sub.url.as_deref().unwrap_or("")) .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); let scroll = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .child(&content) .build(); toolbar.set_content(Some(&scroll)); dialog.set_child(Some(&toolbar)); { let sub_id = sub.id; let sub_active = sub.active; let sub_recurring_id = sub.recurring_id; let db_ref = db.clone(); let dialog_ref = dialog.clone(); let active_ref = active_group.clone(); let paused_ref = paused_group.clone(); let monthly_ref = monthly_label.clone(); let yearly_ref = yearly_label.clone(); let base_ref = base_currency.to_string(); let toast_ref = toast_overlay.clone(); let cat_ids = cat_ids.clone(); let currency_codes = currency_codes.clone(); let change_ref = on_change.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 a name")); return; } let amount_text = amount_row.text().to_string(); let amount: f64 = match amount_text.parse() { Ok(v) if v > 0.0 => v, _ => { toast_ref.add_toast(adw::Toast::new("Please enter a valid amount")); return; } }; let currency = currency_codes .get(currency_row.selected() as usize) .cloned() .unwrap_or_else(|| "USD".to_string()); let frequency = match freq_row.selected() { 0 => Frequency::Daily, 1 => Frequency::Weekly, 2 => Frequency::Biweekly, 3 => Frequency::Monthly, _ => Frequency::Yearly, }; let cat_idx = cat_row.selected() as usize; let ids = cat_ids.borrow(); let category_id = match ids.get(cat_idx) { Some(&id) => id, None => { toast_ref.add_toast(adw::Toast::new("Please select a category")); return; } }; let start_str = start_date_label.text().to_string(); let start_date = NaiveDate::parse_from_str(&start_str, "%Y-%m-%d") .unwrap_or_else(|_| chrono::Local::now().date_naive()); let note_text = note_row.text(); let note = if note_text.is_empty() { None } else { Some(note_text.to_string()) }; let url_text = url_row.text(); let url = if url_text.is_empty() { None } else { Some(url_text.to_string()) }; let updated = Subscription { id: sub_id, name: name.trim().to_string(), amount, currency: currency.clone(), frequency, category_id, start_date, next_due: start_date, active: sub_active, note: note.clone(), url, recurring_id: sub_recurring_id, }; match db_ref.update_subscription(&updated) { Ok(_) => { if let Some(rec_id) = sub_recurring_id { if let Ok(mut rec) = db_ref.get_recurring(rec_id) { rec.amount = amount; rec.frequency = frequency; rec.currency = currency; rec.note = note; let _ = db_ref.update_recurring(&rec); } } dialog_ref.close(); Self::load_subscriptions( &db_ref, &active_ref, &paused_ref, &monthly_ref, &yearly_ref, &base_ref, &toast_ref, &change_ref, ); Self::notify_change(&change_ref); toast_ref.add_toast(adw::Toast::new("Subscription updated")); } Err(e) => { toast_ref.add_toast(adw::Toast::new(&format!("Error: {}", e))); } } }); } dialog.present(Some(parent)); } fn make_category_factory() -> gtk::SignalListItemFactory { let factory = gtk::SignalListItemFactory::new(); factory.connect_setup(|_, item| { let item = item.downcast_ref::().unwrap(); let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 8); let icon = gtk::Image::new(); icon.set_pixel_size(20); let label = gtk::Label::new(None); hbox.append(&icon); hbox.append(&label); item.set_child(Some(&hbox)); }); factory.connect_bind(|_, item| { let item = item.downcast_ref::().unwrap(); let string_obj = item.item().and_downcast::().unwrap(); let text = string_obj.string(); let hbox = item.child().and_downcast::().unwrap(); let icon = hbox.first_child().and_downcast::().unwrap(); let label = icon.next_sibling().and_downcast::().unwrap(); if let Some((icon_name, name)) = text.split_once('\t') { icon.set_icon_name(Some(icon_name)); icon.set_visible(true); label.set_label(name); } else { icon.set_visible(false); label.set_label(&text); } }); factory } 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(); } } }