Add GitHub metadata enrichment for catalog apps

Enrich catalog apps with GitHub API data (stars, version, downloads,
release date) via two strategies: background drip for repo-level info
and on-demand fetch when opening a detail page.

- Add github_enrichment module with API calls, asset filtering, and
  architecture auto-detection for AppImage downloads
- DB migrations v14/v15 for GitHub metadata and release asset columns
- Extract github_owner/repo from feed links during catalog sync
- Display colored stat cards (stars, version, downloads, released) on
  detail pages with on-demand enrichment
- Show stars and version on browse tiles and featured carousel cards
- Replace install button with SplitButton dropdown when multiple arch
  assets available, preferring detected architecture
- Disable install button until enrichment completes to prevent stale
  AppImageHub URL downloads
- Keep enrichment banner visible on catalog page until truly complete,
  showing paused state when rate-limited
- Add GitHub token and auto-enrich toggle to preferences
This commit is contained in:
lashman
2026-02-28 16:49:13 +02:00
parent 92c51dc39e
commit f89aafca6a
15 changed files with 3027 additions and 224 deletions

View File

@@ -589,13 +589,13 @@ impl LibraryView {
// Grid card
let card = app_card::build_app_card(record);
let card_menu = build_context_menu(record);
attach_context_menu(&card, &card_menu);
attach_context_menu(&card, &card_menu, record.id);
self.flow_box.append(&card);
// List row
let row = self.build_list_row(record);
let row_menu = build_context_menu(record);
attach_context_menu(&row, &row_menu);
attach_context_menu(&row, &row_menu, record.id);
self.list_box.append(&row);
}
@@ -812,15 +812,39 @@ fn build_context_menu(record: &AppImageRecord) -> gtk::gio::Menu {
section4.append(Some("Copy file location"), Some(&format!("win.copy-path(int64 {})", record.id)));
menu.append_section(None, &section4);
// Section 5: Destructive actions
let section5 = gtk::gio::Menu::new();
let uninstall_item = gtk::gio::MenuItem::new(None, Some(&format!("win.uninstall-appimage(int64 {})", record.id)));
uninstall_item.set_attribute_value("custom", Some(&"uninstall".to_variant()));
section5.append_item(&uninstall_item);
menu.append_section(None, &section5);
menu
}
/// Attach a right-click context menu to a widget.
fn attach_context_menu(widget: &impl gtk::prelude::IsA<gtk::Widget>, menu_model: &gtk::gio::Menu) {
let popover = gtk::PopoverMenu::from_model(Some(menu_model));
fn attach_context_menu(widget: &impl gtk::prelude::IsA<gtk::Widget>, menu_model: &gtk::gio::Menu, record_id: i64) {
let popover = gtk::PopoverMenu::from_model_full(menu_model, gtk::PopoverMenuFlags::NESTED);
popover.set_parent(widget.as_ref());
popover.set_has_arrow(false);
// Add custom destructive-styled uninstall button
let uninstall_btn = gtk::Button::builder()
.label("Uninstall")
.build();
uninstall_btn.add_css_class("destructive-context-item");
// Left-align the label to match other menu items
if let Some(label) = uninstall_btn.child().and_then(|c| c.downcast::<gtk::Label>().ok()) {
label.set_halign(gtk::Align::Start);
}
uninstall_btn.set_action_name(Some("win.uninstall-appimage"));
uninstall_btn.set_action_target_value(Some(&record_id.to_variant()));
let popover_ref = popover.clone();
uninstall_btn.connect_clicked(move |_| {
popover_ref.popdown();
});
popover.add_child(&uninstall_btn, "uninstall");
// Unparent the popover when the widget is destroyed to avoid GTK warnings
let popover_cleanup = popover.clone();
widget.as_ref().connect_destroy(move |_| {