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='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'/>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user