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