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.
This commit is contained in:
lashman
2026-02-28 01:36:47 +02:00
parent b23f9e14f8
commit 46f46db98c
3 changed files with 116 additions and 14 deletions

View File

@@ -26,10 +26,20 @@
<choice value='grid'/> <choice value='grid'/>
<choice value='list'/> <choice value='list'/>
</choices> </choices>
<default>'grid'</default> <default>'list'</default>
<summary>Library view mode</summary> <summary>Library view mode</summary>
<description>The library view mode: grid or list.</description> <description>The library view mode: grid or list.</description>
</key> </key>
<key name="sort-mode" type="s">
<choices>
<choice value='name'/>
<choice value='recently-added'/>
<choice value='size'/>
</choices>
<default>'name'</default>
<summary>Library sort mode</summary>
<description>How to sort the library: name, recently-added, or size.</description>
</key>
<key name="color-scheme" type="s"> <key name="color-scheme" type="s">
<choices> <choices>
<choice value='default'/> <choice value='default'/>

View File

@@ -15,6 +15,13 @@ pub enum ViewMode {
List, List,
} }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SortMode {
NameAsc,
RecentlyAdded,
Size,
}
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum LibraryState { pub enum LibraryState {
Loading, Loading,
@@ -33,6 +40,7 @@ pub struct LibraryView {
search_entry: gtk::SearchEntry, search_entry: gtk::SearchEntry,
title_widget: adw::WindowTitle, title_widget: adw::WindowTitle,
view_mode: Rc<Cell<ViewMode>>, view_mode: Rc<Cell<ViewMode>>,
sort_mode: Rc<Cell<SortMode>>,
grid_button: gtk::ToggleButton, grid_button: gtk::ToggleButton,
list_button: gtk::ToggleButton, list_button: gtk::ToggleButton,
records: Rc<RefCell<Vec<AppImageRecord>>>, records: Rc<RefCell<Vec<AppImageRecord>>>,
@@ -59,6 +67,15 @@ impl LibraryView {
}; };
let view_mode = Rc::new(Cell::new(initial_mode)); 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 --- // --- Header bar ---
let menu_button = gtk::MenuButton::builder() let menu_button = gtk::MenuButton::builder()
.icon_name("open-menu-symbolic") .icon_name("open-menu-symbolic")
@@ -103,6 +120,29 @@ impl LibraryView {
.title("Driftwood") .title("Driftwood")
.build(); .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) // Add button (shows drop overlay)
let add_button_icon = gtk::Image::from_icon_name("list-add-symbolic"); let add_button_icon = gtk::Image::from_icon_name("list-add-symbolic");
let add_button_label = gtk::Label::new(Some(&i18n("Add app"))); let add_button_label = gtk::Label::new(Some(&i18n("Add app")));
@@ -131,10 +171,12 @@ impl LibraryView {
let header_bar = adw::HeaderBar::builder() let header_bar = adw::HeaderBar::builder()
.title_widget(&title_widget) .title_widget(&title_widget)
.build(); .build();
header_bar.pack_start(&scan_button);
header_bar.pack_start(&add_button); header_bar.pack_start(&add_button);
header_bar.pack_start(&select_button); header_bar.pack_start(&select_button);
header_bar.pack_end(&menu_button); header_bar.pack_end(&menu_button);
header_bar.pack_end(&search_button); header_bar.pack_end(&search_button);
header_bar.pack_end(&sort_button);
header_bar.pack_end(&view_toggle_box); header_bar.pack_end(&view_toggle_box);
// --- Search bar --- // --- Search bar ---
@@ -187,30 +229,28 @@ impl LibraryView {
.build(); .build();
let scan_now_btn = gtk::Button::builder() let scan_now_btn = gtk::Button::builder()
.label(&i18n("Scan Now")) .label(&i18n("Scan for AppImages"))
.build(); .build();
scan_now_btn.add_css_class("suggested-action"); scan_now_btn.add_css_class("suggested-action");
scan_now_btn.add_css_class("pill"); scan_now_btn.add_css_class("pill");
scan_now_btn.update_property(&[AccessibleProperty::Label("Scan for AppImages")]); scan_now_btn.update_property(&[AccessibleProperty::Label("Scan for AppImages")]);
let prefs_btn = gtk::Button::builder() let browse_catalog_btn = gtk::Button::builder()
.label(&i18n("Preferences")) .label(&i18n("Browse Catalog"))
.build(); .build();
prefs_btn.add_css_class("flat"); browse_catalog_btn.add_css_class("flat");
prefs_btn.add_css_class("pill"); browse_catalog_btn.add_css_class("pill");
prefs_btn.update_property(&[AccessibleProperty::Label("Open preferences")]); 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(&scan_now_btn);
empty_button_box.append(&prefs_btn); empty_button_box.append(&browse_catalog_btn);
let empty_page = adw::StatusPage::builder() let empty_page = adw::StatusPage::builder()
.icon_name("application-x-executable-symbolic") .icon_name("application-x-executable-symbolic")
.title(&i18n("No AppImages Found")) .title(&i18n("No AppImages Yet"))
.description(&i18n( .description(&i18n(
"Driftwood manages your AppImage collection - scanning for apps, \ "Drag and drop AppImage files here, or scan your system to find them.",
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.",
)) ))
.child(&empty_button_box) .child(&empty_button_box)
.build(); .build();
@@ -458,7 +498,6 @@ impl LibraryView {
// --- Wire up empty state buttons --- // --- Wire up empty state buttons ---
scan_now_btn.set_action_name(Some("win.scan")); scan_now_btn.set_action_name(Some("win.scan"));
prefs_btn.set_action_name(Some("win.preferences"));
Self { Self {
page, page,
@@ -470,6 +509,7 @@ impl LibraryView {
search_entry, search_entry,
title_widget, title_widget,
view_mode, view_mode,
sort_mode,
grid_button, grid_button,
list_button, list_button,
records, records,
@@ -515,6 +555,24 @@ impl LibraryView {
self.list_box.remove(&row); 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 // Build cards and list rows
for record in &new_records { for record in &new_records {
// Grid card // Grid card
@@ -684,6 +742,15 @@ impl LibraryView {
self.select_button.set_active(false); 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. /// Programmatically set the view mode by toggling the linked buttons.
pub fn set_view_mode(&self, mode: ViewMode) { pub fn set_view_mode(&self, mode: ViewMode) {
match mode { match mode {

View File

@@ -649,6 +649,31 @@ impl DriftwoodWindow {
show_drop_hint_action, 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::<String>()) 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 --- // --- Batch actions ---
let batch_integrate_action = gio::SimpleAction::new("batch-integrate", None); let batch_integrate_action = gio::SimpleAction::new("batch-integrate", None);
{ {