diff --git a/src/ui/catalog_view.rs b/src/ui/catalog_view.rs index 66ef369..cd98cea 100644 --- a/src/ui/catalog_view.rs +++ b/src/ui/catalog_view.rs @@ -95,20 +95,10 @@ pub fn build_catalog_page(db: &Rc) -> adw::NavigationPage { stack.add_named(&scrolled, Some("results")); // Show empty or results based on catalog data - let sources = catalog::get_sources(db); - let enabled_sources: Vec<_> = sources.iter().filter(|s| s.enabled).collect(); let app_count = db.catalog_app_count().unwrap_or(0); if app_count > 0 { stack.set_visible_child_name("results"); - if let Some(src) = enabled_sources.first() { - let synced = src.last_synced.as_deref().unwrap_or("never"); - title.set_subtitle(&format!( - "{} apps from {} ({}), synced: {}", - src.app_count, src.name, src.source_type.as_str(), synced, - )); - } else { - title.set_subtitle(&format!("{} apps available", app_count)); - } + update_catalog_subtitle(&title, app_count); } else { stack.set_visible_child_name("empty"); } @@ -195,7 +185,7 @@ pub fn build_catalog_page(db: &Rc) -> adw::NavigationPage { toast_c.add_toast(adw::Toast::new( &format!("Catalog refreshed: {} apps", count), )); - title_c.set_subtitle(&format!("{} apps available", count)); + update_catalog_subtitle(&title_c, count as i64); stack_c.set_visible_child_name("results"); populate_categories(&db_c, &cat_box_c, &active_cat_c, &results_c, &search_c); populate_results(&db_c, "", None, &results_c, &toast_c); @@ -221,6 +211,11 @@ pub fn build_catalog_page(db: &Rc) -> adw::NavigationPage { wire_refresh(&refresh_btn); wire_refresh(&refresh_header_btn); + // Auto-refresh on first visit when catalog is empty + if app_count == 0 { + refresh_btn.emit_clicked(); + } + adw::NavigationPage::builder() .title(&i18n("Catalog")) .tag("catalog") @@ -228,6 +223,18 @@ pub fn build_catalog_page(db: &Rc) -> adw::NavigationPage { .build() } +/// Update the catalog subtitle to show app count and relative last-refreshed time. +fn update_catalog_subtitle(title: &adw::WindowTitle, app_count: i64) { + let settings = gtk::gio::Settings::new(crate::config::APP_ID); + let last_refreshed = settings.string("catalog-last-refreshed"); + if last_refreshed.is_empty() { + title.set_subtitle(&format!("{} apps available", app_count)); + } else { + let relative = widgets::relative_time(&last_refreshed); + title.set_subtitle(&format!("{} apps - Refreshed {}", app_count, relative)); + } +} + fn populate_results( db: &Rc, query: &str, @@ -254,6 +261,14 @@ fn populate_results( let toast_ref = toast_overlay.clone(); let db_ref = db.clone(); + // Get installed app names for matching + let installed_names: std::collections::HashSet = db + .get_all_appimages() + .unwrap_or_default() + .iter() + .filter_map(|r| r.app_name.as_ref().map(|n| n.to_lowercase())) + .collect(); + for app in &results { let row = adw::ActionRow::builder() .title(&app.name) @@ -280,6 +295,16 @@ fn populate_results( } } + // Show "Installed" badge if already installed, otherwise show Install button + let is_installed = installed_names.contains(&app.name.to_lowercase()); + if is_installed { + let installed_badge = widgets::status_badge(&i18n("Installed"), "success"); + installed_badge.set_valign(gtk::Align::Center); + row.add_suffix(&installed_badge); + list_box.append(&row); + continue; + } + // Install button let install_btn = gtk::Button::builder() .label(&i18n("Install"))