mimick/library/
actions.rs1use std::cell::Cell;
8use std::rc::Rc;
9
10use glib::clone;
11use gtk::prelude::*;
12use libadwaita::prelude::*;
13
14use crate::library::asset_object::AssetObject;
15
16use super::LOCAL_ID_PREFIX;
17use super::LibraryWindowUi;
18use super::download::start_download_group;
19
20pub(super) fn connect_select_mode(ui: Rc<LibraryWindowUi>, select_toggle: gtk::ToggleButton) {
21 let selection = ui.grid.selection.clone();
22 let bulk_bar = ui.bulk_bar.clone();
23 let count_label = ui.bulk_count_label.clone();
24
25 let refresh = {
26 let selection = selection.clone();
27 let bulk_bar = bulk_bar.clone();
28 let count_label = count_label.clone();
29 let select_toggle = select_toggle.clone();
30 Rc::new(move || {
31 let n = selection_count(&selection);
32 bulk_bar.set_reveal_child(select_toggle.is_active() && n > 0);
33 count_label.set_label(&format!("{} selected", n));
34 })
35 };
36
37 selection.connect_selection_changed({
38 let refresh = refresh.clone();
39 move |_, _, _| (*refresh)()
40 });
41
42 select_toggle.connect_toggled({
43 let selection = selection.clone();
44 let refresh = refresh.clone();
45 move |toggle| {
46 if !toggle.is_active() {
47 selection.unselect_all();
48 }
49 (*refresh)();
50 }
51 });
52
53 let transient = Rc::new(Cell::new(false));
58 let key_controller = gtk::EventControllerKey::new();
59 key_controller.set_propagation_phase(gtk::PropagationPhase::Capture);
60 key_controller.connect_key_pressed({
61 let select_toggle = select_toggle.clone();
62 let transient = transient.clone();
63 move |_, keyval, _, _| {
64 if keyval == gtk::gdk::Key::Escape && select_toggle.is_active() {
65 select_toggle.set_active(false);
66 transient.set(false);
67 return glib::Propagation::Stop;
68 }
69 if matches!(keyval, gtk::gdk::Key::Control_L | gtk::gdk::Key::Control_R)
70 && !select_toggle.is_active()
71 {
72 select_toggle.set_active(true);
73 transient.set(true);
74 }
75 glib::Propagation::Proceed
76 }
77 });
78 key_controller.connect_key_released({
79 let select_toggle = select_toggle.clone();
80 let selection = selection.clone();
81 let transient = transient.clone();
82 move |_, keyval, _, _| {
83 if !matches!(keyval, gtk::gdk::Key::Control_L | gtk::gdk::Key::Control_R)
84 || !transient.get()
85 {
86 return;
87 }
88 if selection.selection().size() == 0 {
89 select_toggle.set_active(false);
90 }
91 transient.set(false);
92 }
93 });
94 ui.window.add_controller(key_controller);
95}
96
97pub(super) fn connect_bulk_actions(
98 ui: Rc<LibraryWindowUi>,
99 delete_btn: gtk::Button,
100 download_btn: gtk::Button,
101 clear_btn: gtk::Button,
102) {
103 clear_btn.connect_clicked(clone!(
104 #[strong]
105 ui,
106 move |_| {
107 ui.grid.selection.unselect_all();
108 ui.select_toggle.set_active(false);
109 }
110 ));
111
112 download_btn.connect_clicked(clone!(
113 #[strong]
114 ui,
115 move |_| {
116 let downloads: Vec<(String, String)> = collect_selected_assets(&ui)
117 .into_iter()
118 .filter(|(asset_id, _)| !asset_id.starts_with(LOCAL_ID_PREFIX))
119 .collect();
120 if !downloads.is_empty() {
121 start_download_group(ui.clone(), downloads);
122 }
123 }
124 ));
125
126 delete_btn.connect_clicked(clone!(
127 #[strong]
128 ui,
129 move |_| {
130 confirm_and_bulk_delete(ui.clone());
131 }
132 ));
133}
134
135pub(super) fn selection_count(selection: >k::MultiSelection) -> u32 {
136 let bitset = selection.selection();
137 bitset.size() as u32
138}
139
140pub(super) fn collect_selected_assets(ui: &Rc<LibraryWindowUi>) -> Vec<(String, String)> {
141 let bitset = ui.grid.selection.selection();
142 let mut out = Vec::new();
143 let Some((mut iter, first)) = gtk::BitsetIter::init_first(&bitset) else {
144 return out;
145 };
146 let mut pos = Some(first);
147 while let Some(p) = pos {
148 if let Some(item) = ui.grid.model.item(p).and_downcast::<AssetObject>() {
149 out.push((
150 item.property::<String>("id"),
151 item.property::<String>("filename"),
152 ));
153 }
154 pos = iter.next();
155 }
156 out
157}
158
159fn confirm_and_bulk_delete(ui: Rc<LibraryWindowUi>) {
160 let assets = collect_selected_assets(&ui);
161 let remote_ids: Vec<String> = assets
162 .iter()
163 .filter(|(id, _)| !id.starts_with(LOCAL_ID_PREFIX))
164 .map(|(id, _)| id.clone())
165 .collect();
166 if remote_ids.is_empty() {
167 return;
168 }
169
170 let dialog = libadwaita::AlertDialog::builder()
171 .heading(format!("Move {} item(s) to trash?", remote_ids.len()))
172 .body("Items can be restored from the Immich trash.")
173 .build();
174 dialog.add_response("cancel", "Cancel");
175 dialog.add_response("delete", "Move to trash");
176 dialog.set_response_appearance("delete", libadwaita::ResponseAppearance::Destructive);
177 dialog.set_default_response(Some("cancel"));
178 dialog.set_close_response("cancel");
179
180 let ui_for_choice = ui.clone();
181 dialog.connect_response(None, move |dlg, response| {
182 if response != "delete" {
183 return;
184 }
185 let ui = ui_for_choice.clone();
186 let ids = remote_ids.clone();
187 glib::MainContext::default().spawn_local(async move {
188 match ui.ctx.api_client.delete_assets(&ids).await {
189 Ok(()) => {
190 ui.grid.selection.unselect_all();
191 ui.select_toggle.set_active(false);
192 super::refresh_library_after_mutation(ui.clone(), true);
193 }
194 Err(err) => {
195 log::error!("Bulk delete failed: {}", err);
196 }
197 }
198 });
199 dlg.close();
200 });
201 dialog.present(Some(&ui.window));
202}