Add scan button, sort dropdown, and improved empty state to library view
This commit is contained in:
@@ -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'/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user