Add scan button, sort dropdown, and improved empty state to library view

This commit is contained in:
2026-02-28 01:36:47 +02:00
parent f431cea768
commit a187316a1a
3 changed files with 116 additions and 14 deletions

View File

@@ -26,10 +26,20 @@
<choice value='grid'/>
<choice value='list'/>
</choices>
<default>'grid'</default>
<default>'list'</default>
<summary>Library view mode</summary>
<description>The library view mode: grid or list.</description>
</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">
<choices>
<choice value='default'/>

View File

@@ -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<Cell<ViewMode>>,
sort_mode: Rc<Cell<SortMode>>,
grid_button: gtk::ToggleButton,
list_button: gtk::ToggleButton,
records: Rc<RefCell<Vec<AppImageRecord>>>,
@@ -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 {

View File

@@ -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::<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 ---
let batch_integrate_action = gio::SimpleAction::new("batch-integrate", None);
{