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); {