Add UX enhancements: carousel, filter chips, command palette, and more

- Replace featured section Stack with AdwCarousel + indicator dots
- Convert category grid to horizontal scrollable filter chips
- Add grid/list view toggle for catalog with compact row layout
- Add quick launch button on library list rows
- Add stale catalog banner when data is older than 7 days
- Add command palette (Ctrl+K) for quick app search and launch
- Show specific app names in update notifications
- Add per-app auto-update toggle (skip updates switch)
- Add keyboard shortcut hints to button tooltips
- Add source trust badges (AppImageHub/Community) on catalog tiles
- Add undo-based uninstall with toast and record restoration
- Add type-to-search in library view
- Use human-readable catalog source labels
- Show Launch button for installed apps in catalog detail
- Replace external browser link with inline AppImage explainer dialog
This commit is contained in:
lashman
2026-03-01 00:39:43 +02:00
parent 4b939f044a
commit d11546efc6
25 changed files with 1711 additions and 481 deletions

View File

@@ -89,7 +89,7 @@ impl LibraryView {
let search_button = gtk::ToggleButton::builder()
.icon_name("system-search-symbolic")
.tooltip_text(&i18n("Search"))
.tooltip_text(&i18n("Search (Ctrl+F)"))
.build();
search_button.add_css_class("flat");
search_button.update_property(&[AccessibleProperty::Label("Toggle search")]);
@@ -124,7 +124,7 @@ impl LibraryView {
// Scan button
let scan_button = gtk::Button::builder()
.icon_name("view-refresh-symbolic")
.tooltip_text(&i18n("Scan for AppImages"))
.tooltip_text(&i18n("Scan for AppImages (Ctrl+R)"))
.build();
scan_button.add_css_class("flat");
scan_button.set_action_name(Some("win.scan"));
@@ -244,14 +244,44 @@ impl LibraryView {
browse_catalog_btn.set_action_name(Some("win.catalog"));
browse_catalog_btn.update_property(&[AccessibleProperty::Label("Browse app catalog")]);
let learn_btn = gtk::Button::builder()
.label(&i18n("What is an AppImage?"))
.build();
learn_btn.add_css_class("flat");
learn_btn.add_css_class("pill");
learn_btn.connect_clicked(|btn| {
let dialog = adw::AlertDialog::builder()
.heading(&i18n("What is an AppImage?"))
.body(&i18n(
"AppImages are self-contained app files for Linux, similar to .exe files on Windows or .dmg files on Mac.\n\n\
Key differences from traditional Linux packages:\n\
- No installation needed - just download and run\n\
- One file per app - easy to back up and share\n\
- Works on most Linux distributions\n\
- Does not require admin/root access\n\n\
Driftwood helps you discover, organize, and keep your AppImages up to date."
))
.build();
dialog.add_response("learn-more", &i18n("Learn More Online"));
dialog.add_response("ok", &i18n("Got It"));
dialog.set_default_response(Some("ok"));
dialog.set_close_response("ok");
dialog.connect_response(Some("learn-more"), |_, _| {
gtk::UriLauncher::new("https://appimage.org")
.launch(gtk::Window::NONE, gtk::gio::Cancellable::NONE, |_| {});
});
dialog.present(Some(btn));
});
empty_button_box.append(&scan_now_btn);
empty_button_box.append(&browse_catalog_btn);
empty_button_box.append(&learn_btn);
let empty_page = adw::StatusPage::builder()
.icon_name("application-x-executable-symbolic")
.title(&i18n("No AppImages Yet"))
.description(&i18n(
"Drag and drop AppImage files here, or scan your system to find them.",
"AppImages are portable apps for Linux - like .exe files, but they run without installation. Drag one here, scan your system, or browse the catalog to get started.",
))
.child(&empty_button_box)
.build();
@@ -361,6 +391,9 @@ impl LibraryView {
toolbar_view.set_content(Some(&content_box));
widgets::apply_pointer_cursors(&toolbar_view);
// Enable type-to-search: any keypress in the view opens the search bar
search_bar.set_key_capture_widget(Some(&toolbar_view));
let page = adw::NavigationPage::builder()
.title("Driftwood")
.tag("library")
@@ -670,6 +703,18 @@ impl LibraryView {
icon.add_css_class("icon-rounded");
row.add_prefix(&icon);
// Quick launch button
let launch_btn = gtk::Button::builder()
.icon_name("media-playback-start-symbolic")
.tooltip_text(&i18n("Launch"))
.css_classes(["flat", "circular"])
.valign(gtk::Align::Center)
.build();
launch_btn.set_action_name(Some("win.launch-appimage"));
launch_btn.set_action_target_value(Some(&record.id.to_variant()));
widgets::set_pointer_cursor(&launch_btn);
row.add_suffix(&launch_btn);
// Single most important badge as suffix (same priority as cards)
if let Some(badge) = app_card::build_priority_badge(record) {
badge.set_valign(gtk::Align::Center);
@@ -792,7 +837,7 @@ fn build_context_menu(record: &AppImageRecord) -> gtk::gio::Menu {
// Section 1: Launch
let section1 = gtk::gio::Menu::new();
section1.append(Some("Launch"), Some(&format!("win.launch-appimage(int64 {})", record.id)));
section1.append(Some("Open"), Some(&format!("win.launch-appimage(int64 {})", record.id)));
menu.append_section(None, &section1);
// Section 2: Actions
@@ -803,7 +848,7 @@ fn build_context_menu(record: &AppImageRecord) -> gtk::gio::Menu {
// Section 3: Integration + folder
let section3 = gtk::gio::Menu::new();
let integrate_label = if record.integrated { "Remove from app menu" } else { "Add to app menu" };
let integrate_label = if record.integrated { "Remove from launcher" } else { "Add to launcher" };
section3.append(Some(integrate_label), Some(&format!("win.toggle-integration(int64 {})", record.id)));
section3.append(Some("Show in file manager"), Some(&format!("win.open-folder(int64 {})", record.id)));
menu.append_section(None, &section3);