From 46f46db98c18e67bb686fcb177782054dc5375ec Mon Sep 17 00:00:00 2001 From: lashman Date: Sat, 28 Feb 2026 01:36:47 +0200 Subject: [PATCH] Add scan button, sort dropdown, and improved empty state to library view Add a scan button and sort dropdown (Name A-Z, Recently Added, Size) to the library header bar. Change the empty state to friendlier text with Scan and Browse Catalog buttons. Default view mode changed to list. Sort preference persisted via GSettings. --- data/app.driftwood.Driftwood.gschema.xml | 12 ++- src/ui/library_view.rs | 93 ++++++++++++++++++++---- src/window.rs | 25 +++++++ 3 files changed, 116 insertions(+), 14 deletions(-) diff --git a/data/app.driftwood.Driftwood.gschema.xml b/data/app.driftwood.Driftwood.gschema.xml index b5a1908..cf73e71 100644 --- a/data/app.driftwood.Driftwood.gschema.xml +++ b/data/app.driftwood.Driftwood.gschema.xml @@ -26,10 +26,20 @@ - 'grid' + 'list' Library view mode The library view mode: grid or list. + + + + + + + 'name' + Library sort mode + How to sort the library: name, recently-added, or size. + diff --git a/src/ui/library_view.rs b/src/ui/library_view.rs index b447373..51b84f3 100644 --- a/src/ui/library_view.rs +++ b/src/ui/library_view.rs @@ -15,6 +15,13 @@ pub enum ViewMode { List, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SortMode { + NameAsc, + RecentlyAdded, + Size, +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum LibraryState { Loading, @@ -33,6 +40,7 @@ pub struct LibraryView { search_entry: gtk::SearchEntry, title_widget: adw::WindowTitle, view_mode: Rc>, + sort_mode: Rc>, grid_button: gtk::ToggleButton, list_button: gtk::ToggleButton, records: Rc>>, @@ -59,6 +67,15 @@ impl LibraryView { }; let view_mode = Rc::new(Cell::new(initial_mode)); + // Sort mode from settings + let saved_sort = settings.string("sort-mode"); + let initial_sort = match saved_sort.as_str() { + "recently-added" => SortMode::RecentlyAdded, + "size" => SortMode::Size, + _ => SortMode::NameAsc, + }; + let sort_mode = Rc::new(Cell::new(initial_sort)); + // --- Header bar --- let menu_button = gtk::MenuButton::builder() .icon_name("open-menu-symbolic") @@ -103,6 +120,29 @@ impl LibraryView { .title("Driftwood") .build(); + // Scan button + let scan_button = gtk::Button::builder() + .icon_name("view-refresh-symbolic") + .tooltip_text(&i18n("Scan for AppImages")) + .build(); + scan_button.add_css_class("flat"); + scan_button.set_action_name(Some("win.scan")); + scan_button.update_property(&[AccessibleProperty::Label("Scan for AppImages")]); + + // Sort dropdown + let sort_menu = gtk::gio::Menu::new(); + sort_menu.append(Some(&i18n("Name A-Z")), Some("win.sort-library::name")); + sort_menu.append(Some(&i18n("Recently Added")), Some("win.sort-library::recent")); + sort_menu.append(Some(&i18n("Size")), Some("win.sort-library::size")); + + let sort_button = gtk::MenuButton::builder() + .icon_name("view-sort-descending-symbolic") + .menu_model(&sort_menu) + .tooltip_text(&i18n("Sort")) + .build(); + sort_button.add_css_class("flat"); + sort_button.update_property(&[AccessibleProperty::Label("Sort library")]); + // Add button (shows drop overlay) let add_button_icon = gtk::Image::from_icon_name("list-add-symbolic"); let add_button_label = gtk::Label::new(Some(&i18n("Add app"))); @@ -131,10 +171,12 @@ impl LibraryView { let header_bar = adw::HeaderBar::builder() .title_widget(&title_widget) .build(); + header_bar.pack_start(&scan_button); header_bar.pack_start(&add_button); header_bar.pack_start(&select_button); header_bar.pack_end(&menu_button); header_bar.pack_end(&search_button); + header_bar.pack_end(&sort_button); header_bar.pack_end(&view_toggle_box); // --- Search bar --- @@ -187,30 +229,28 @@ impl LibraryView { .build(); let scan_now_btn = gtk::Button::builder() - .label(&i18n("Scan Now")) + .label(&i18n("Scan for AppImages")) .build(); scan_now_btn.add_css_class("suggested-action"); scan_now_btn.add_css_class("pill"); scan_now_btn.update_property(&[AccessibleProperty::Label("Scan for AppImages")]); - let prefs_btn = gtk::Button::builder() - .label(&i18n("Preferences")) + let browse_catalog_btn = gtk::Button::builder() + .label(&i18n("Browse Catalog")) .build(); - prefs_btn.add_css_class("flat"); - prefs_btn.add_css_class("pill"); - prefs_btn.update_property(&[AccessibleProperty::Label("Open preferences")]); + browse_catalog_btn.add_css_class("flat"); + browse_catalog_btn.add_css_class("pill"); + browse_catalog_btn.set_action_name(Some("win.catalog")); + browse_catalog_btn.update_property(&[AccessibleProperty::Label("Browse app catalog")]); empty_button_box.append(&scan_now_btn); - empty_button_box.append(&prefs_btn); + empty_button_box.append(&browse_catalog_btn); let empty_page = adw::StatusPage::builder() .icon_name("application-x-executable-symbolic") - .title(&i18n("No AppImages Found")) + .title(&i18n("No AppImages Yet")) .description(&i18n( - "Driftwood manages your AppImage collection - scanning for apps, \ - integrating them into your desktop, and keeping them up to date.\n\n\ - Drag AppImage files here, or add them to ~/Applications or ~/Downloads, \ - then use Scan Now to find them.", + "Drag and drop AppImage files here, or scan your system to find them.", )) .child(&empty_button_box) .build(); @@ -458,7 +498,6 @@ impl LibraryView { // --- Wire up empty state buttons --- scan_now_btn.set_action_name(Some("win.scan")); - prefs_btn.set_action_name(Some("win.preferences")); Self { page, @@ -470,6 +509,7 @@ impl LibraryView { search_entry, title_widget, view_mode, + sort_mode, grid_button, list_button, records, @@ -515,6 +555,24 @@ impl LibraryView { self.list_box.remove(&row); } + // Sort records based on current sort mode + let mut new_records = new_records; + match self.sort_mode.get() { + SortMode::NameAsc => { + new_records.sort_by(|a, b| { + let name_a = a.app_name.as_deref().unwrap_or(&a.filename).to_lowercase(); + let name_b = b.app_name.as_deref().unwrap_or(&b.filename).to_lowercase(); + name_a.cmp(&name_b) + }); + } + SortMode::RecentlyAdded => { + new_records.sort_by(|a, b| b.first_seen.cmp(&a.first_seen)); + } + SortMode::Size => { + new_records.sort_by(|a, b| b.size_bytes.cmp(&a.size_bytes)); + } + } + // Build cards and list rows for record in &new_records { // Grid card @@ -684,6 +742,15 @@ impl LibraryView { self.select_button.set_active(false); } + /// Set the sort mode and re-populate with current records. + pub fn set_sort_mode(&self, mode: SortMode) { + self.sort_mode.set(mode); + let records = self.records.borrow().clone(); + if !records.is_empty() { + self.populate(records); + } + } + /// Programmatically set the view mode by toggling the linked buttons. pub fn set_view_mode(&self, mode: ViewMode) { match mode { diff --git a/src/window.rs b/src/window.rs index 58d0f91..6d472a5 100644 --- a/src/window.rs +++ b/src/window.rs @@ -649,6 +649,31 @@ impl DriftwoodWindow { show_drop_hint_action, ]); + // Sort library action (parameterized with sort mode string) + let sort_action = gio::SimpleAction::new("sort-library", Some(glib::VariantTy::STRING)); + { + let window_weak = self.downgrade(); + sort_action.connect_activate(move |_, param| { + let Some(window) = window_weak.upgrade() else { return }; + let Some(mode_str) = param.and_then(|p| p.get::()) else { return }; + let lib_view = window.imp().library_view.get().unwrap(); + let sort_mode = match mode_str.as_str() { + "recent" => crate::ui::library_view::SortMode::RecentlyAdded, + "size" => crate::ui::library_view::SortMode::Size, + _ => crate::ui::library_view::SortMode::NameAsc, + }; + lib_view.set_sort_mode(sort_mode); + let settings_key = match mode_str.as_str() { + "recent" => "recently-added", + "size" => "size", + _ => "name", + }; + let settings = gio::Settings::new(APP_ID); + settings.set_string("sort-mode", settings_key).ok(); + }); + } + self.add_action(&sort_action); + // --- Batch actions --- let batch_integrate_action = gio::SimpleAction::new("batch-integrate", None); {