Add batch selection and bulk operations to library view

This commit is contained in:
lashman
2026-02-27 23:45:01 +02:00
parent 5b668bd931
commit 730452072f
2 changed files with 178 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
use adw::prelude::*;
use gtk::accessible::Property as AccessibleProperty;
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::rc::Rc;
use crate::core::database::AppImageRecord;
@@ -36,6 +37,12 @@ pub struct LibraryView {
list_button: gtk::ToggleButton,
records: Rc<RefCell<Vec<AppImageRecord>>>,
search_empty_page: adw::StatusPage,
// Batch selection
selection_mode: Rc<Cell<bool>>,
selected_ids: Rc<RefCell<HashSet<i64>>>,
_action_bar: gtk::ActionBar,
select_button: gtk::ToggleButton,
selection_label: gtk::Label,
}
impl LibraryView {
@@ -114,10 +121,18 @@ impl LibraryView {
add_button.set_action_name(Some("win.show-drop-hint"));
add_button.update_property(&[AccessibleProperty::Label("Add AppImage")]);
let select_button = gtk::ToggleButton::builder()
.icon_name("selection-mode-symbolic")
.tooltip_text(&i18n("Select multiple"))
.build();
select_button.add_css_class("flat");
select_button.update_property(&[AccessibleProperty::Label("Toggle selection mode")]);
let header_bar = adw::HeaderBar::builder()
.title_widget(&title_widget)
.build();
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(&view_toggle_box);
@@ -254,12 +269,42 @@ impl LibraryView {
.build();
stack.add_named(&list_scroll, Some("list"));
// --- Batch selection state ---
let selection_mode = Rc::new(Cell::new(false));
let selected_ids: Rc<RefCell<HashSet<i64>>> = Rc::new(RefCell::new(HashSet::new()));
// --- Bottom action bar (hidden until selection mode) ---
let action_bar = gtk::ActionBar::new();
action_bar.set_visible(false);
let selection_label = gtk::Label::builder()
.label("0 selected")
.build();
action_bar.set_center_widget(Some(&selection_label));
let integrate_btn = gtk::Button::builder()
.label(&i18n("Integrate"))
.tooltip_text(&i18n("Add selected to app menu"))
.build();
integrate_btn.add_css_class("suggested-action");
integrate_btn.set_action_name(Some("win.batch-integrate"));
action_bar.pack_start(&integrate_btn);
let delete_btn = gtk::Button::builder()
.label(&i18n("Delete"))
.tooltip_text(&i18n("Delete selected AppImages"))
.build();
delete_btn.add_css_class("destructive-action");
delete_btn.set_action_name(Some("win.batch-delete"));
action_bar.pack_end(&delete_btn);
// --- Assemble toolbar view ---
let content_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
content_box.append(&search_bar);
content_box.append(&stack);
content_box.append(&action_bar);
let toolbar_view = adw::ToolbarView::new();
toolbar_view.add_top_bar(&header_bar);
@@ -394,6 +439,23 @@ impl LibraryView {
});
}
// --- Wire up selection mode toggle ---
{
let selection_mode_ref = selection_mode.clone();
let selected_ids_ref = selected_ids.clone();
let action_bar_ref = action_bar.clone();
let selection_label_ref = selection_label.clone();
select_button.connect_toggled(move |btn| {
let active = btn.is_active();
selection_mode_ref.set(active);
action_bar_ref.set_visible(active);
if !active {
selected_ids_ref.borrow_mut().clear();
selection_label_ref.set_label("0 selected");
}
});
}
// --- Wire up empty state buttons ---
scan_now_btn.set_action_name(Some("win.scan"));
prefs_btn.set_action_name(Some("win.preferences"));
@@ -412,6 +474,11 @@ impl LibraryView {
list_button,
records,
search_empty_page,
selection_mode,
selected_ids,
_action_bar: action_bar,
select_button,
selection_label,
}
}
@@ -590,6 +657,33 @@ impl LibraryView {
}
}
/// Get the set of currently selected record IDs.
pub fn selected_ids(&self) -> HashSet<i64> {
self.selected_ids.borrow().clone()
}
/// Toggle selection of a record ID (used by card click in selection mode).
pub fn toggle_selection(&self, id: i64) {
let mut ids = self.selected_ids.borrow_mut();
if ids.contains(&id) {
ids.remove(&id);
} else {
ids.insert(id);
}
let count = ids.len();
self.selection_label.set_label(&format!("{} selected", count));
}
/// Whether the library is in selection mode.
pub fn in_selection_mode(&self) -> bool {
self.selection_mode.get()
}
/// Exit selection mode.
pub fn exit_selection_mode(&self) {
self.select_button.set_active(false);
}
/// Programmatically set the view mode by toggling the linked buttons.
pub fn set_view_mode(&self, mode: ViewMode) {
match mode {