mimick/library/
trash_view.rs1use std::rc::Rc;
8
9use gtk::prelude::*;
10use libadwaita::prelude::*;
11
12use super::LibraryWindowUi;
13use super::actions::{TrashTarget, remove_assets_from_grid, selected_items};
14use super::context_menu::show_alert_dialog;
15use super::state::LibrarySource;
16
17pub(super) struct TrashControls {
19 pub bar: gtk::Revealer,
21 restore_all: gtk::Button,
22 empty: gtk::Button,
23 pub restore_selected: gtk::Button,
25 pub delete_selected: gtk::Button,
26 live_only: Vec<gtk::Button>,
28}
29
30fn build_trash_bar(restore_all: >k::Button, empty: >k::Button) -> gtk::Revealer {
32 let note = gtk::Label::builder()
33 .label("Immich permanently deletes trashed items after its retention period (30 days by default).")
34 .xalign(0.0)
35 .hexpand(true)
36 .wrap(true)
37 .css_classes(["dim-label"])
38 .build();
39 let buttons = gtk::Box::builder()
40 .orientation(gtk::Orientation::Horizontal)
41 .spacing(8)
42 .halign(gtk::Align::End)
43 .build();
44 buttons.append(restore_all);
45 buttons.append(empty);
46 let inner = gtk::Box::builder()
49 .orientation(gtk::Orientation::Vertical)
50 .spacing(6)
51 .margin_top(6)
52 .margin_bottom(6)
53 .margin_start(12)
54 .margin_end(12)
55 .build();
56 inner.append(¬e);
57 inner.append(&buttons);
58 gtk::Revealer::builder()
59 .transition_type(gtk::RevealerTransitionType::SlideDown)
60 .reveal_child(false)
61 .child(&inner)
62 .build()
63}
64
65pub(super) fn build_trash_controls(live_only: Vec<gtk::Button>) -> TrashControls {
66 let restore_all = gtk::Button::builder().label("Restore All").build();
67 let empty = gtk::Button::builder()
68 .label("Empty Trash")
69 .css_classes(["destructive-action"])
70 .build();
71 let bar = build_trash_bar(&restore_all, &empty);
72 let restore_selected = gtk::Button::builder()
73 .icon_name("edit-undo-symbolic")
74 .tooltip_text("Restore selected")
75 .visible(false)
76 .build();
77 let delete_selected = gtk::Button::builder()
78 .icon_name("edit-delete-symbolic")
79 .tooltip_text("Delete selected permanently")
80 .css_classes(["destructive-action"])
81 .visible(false)
82 .build();
83 TrashControls {
84 bar,
85 restore_all,
86 empty,
87 restore_selected,
88 delete_selected,
89 live_only,
90 }
91}
92
93pub(super) fn is_trash_active(ui: &LibraryWindowUi) -> bool {
94 matches!(ui.ctx.library_state.lock().source, LibrarySource::Trash)
95}
96
97pub(super) fn set_trash_mode(ui: &LibraryWindowUi, active: bool) {
99 let trash = &ui.trash;
100 trash.bar.set_reveal_child(active);
101 trash.restore_selected.set_visible(active);
102 trash.delete_selected.set_visible(active);
103 for button in &trash.live_only {
104 button.set_visible(!active);
105 }
106}
107
108pub(super) fn connect_trash_controls(ui: Rc<LibraryWindowUi>) {
109 let ui_restore_all = ui.clone();
110 ui.trash
111 .restore_all
112 .connect_clicked(move |_| restore_all(ui_restore_all.clone()));
113 let ui_empty = ui.clone();
114 ui.trash
115 .empty
116 .connect_clicked(move |_| confirm_empty_trash(ui_empty.clone()));
117 let ui_restore = ui.clone();
118 ui.trash
119 .restore_selected
120 .connect_clicked(move |_| restore_selected(ui_restore.clone()));
121 let ui_delete = ui.clone();
122 ui.trash
123 .delete_selected
124 .connect_clicked(move |_| confirm_delete_selected(ui_delete.clone()));
125}
126
127fn selected_ids(ui: &Rc<LibraryWindowUi>) -> Vec<String> {
128 selected_items(ui)
129 .iter()
130 .filter_map(TrashTarget::from_item)
131 .map(|target| target.remote_id().to_string())
132 .collect()
133}
134
135fn clear_selection(ui: &LibraryWindowUi) {
136 ui.grid.selection.unselect_all();
137 ui.select_toggle.set_active(false);
138}
139
140fn restore_selected(ui: Rc<LibraryWindowUi>) {
141 let ids = selected_ids(&ui);
142 let ui_done = ui.clone();
143 restore(ui, ids, move || clear_selection(&ui_done));
144}
145
146fn confirm_delete_selected(ui: Rc<LibraryWindowUi>) {
147 let ids = selected_ids(&ui);
148 let ui_done = ui.clone();
149 confirm_delete_permanently(ui, ids, move || clear_selection(&ui_done));
150}
151
152pub(super) fn restore(ui: Rc<LibraryWindowUi>, ids: Vec<String>, on_done: impl Fn() + 'static) {
154 if ids.is_empty() {
155 return;
156 }
157 glib::MainContext::default().spawn_local(async move {
158 match ui.ctx.api_client.restore_assets(&ids).await {
159 Ok(_) => {
160 remove_assets_from_grid(&ui, &ids);
161 on_done();
162 }
163 Err(err) => show_alert_dialog(&ui, "Could Not Restore", &err),
164 }
165 });
166}
167
168fn permanent_delete_text(count: usize) -> (String, String) {
170 let heading = if count == 1 {
171 "Delete this item permanently?".to_string()
172 } else {
173 format!("Delete {count} items permanently?")
174 };
175 (
176 heading,
177 "This can't be undone. Local copies in your watch folders are kept.".to_string(),
178 )
179}
180
181pub(super) fn confirm_delete_permanently(
183 ui: Rc<LibraryWindowUi>,
184 ids: Vec<String>,
185 on_done: impl Fn() + 'static,
186) {
187 if ids.is_empty() {
188 return;
189 }
190 let (heading, body) = permanent_delete_text(ids.len());
191 let on_done = Rc::new(on_done);
192 confirm_destructive(&ui, &heading, &body, "Delete Permanently", move |ui| {
193 let ids = ids.clone();
194 let on_done = on_done.clone();
195 glib::MainContext::default().spawn_local(async move {
196 match ui.ctx.api_client.delete_assets_permanently(&ids).await {
197 Ok(()) => {
198 remove_assets_from_grid(&ui, &ids);
199 on_done();
200 }
201 Err(err) => show_alert_dialog(&ui, "Could Not Delete", &err),
202 }
203 });
204 });
205}
206
207fn restore_all(ui: Rc<LibraryWindowUi>) {
208 glib::MainContext::default().spawn_local(async move {
209 match ui.ctx.api_client.restore_all_trash().await {
210 Ok(_) => super::refresh_library_after_mutation(ui.clone(), true),
211 Err(err) => show_alert_dialog(&ui, "Could Not Restore", &err),
212 }
213 });
214}
215
216fn confirm_empty_trash(ui: Rc<LibraryWindowUi>) {
217 confirm_destructive(
218 &ui,
219 "Empty the trash?",
220 "Everything in the Immich trash is deleted permanently. This can't be undone.",
221 "Empty Trash",
222 |ui| {
223 glib::MainContext::default().spawn_local(async move {
224 match ui.ctx.api_client.empty_trash().await {
225 Ok(_) => super::refresh_library_after_mutation(ui.clone(), true),
226 Err(err) => show_alert_dialog(&ui, "Could Not Empty Trash", &err),
227 }
228 });
229 },
230 );
231}
232
233fn confirm_destructive(
235 ui: &Rc<LibraryWindowUi>,
236 heading: &str,
237 body: &str,
238 action_label: &str,
239 on_confirm: impl Fn(Rc<LibraryWindowUi>) + 'static,
240) {
241 let dialog = libadwaita::AlertDialog::builder()
242 .heading(heading)
243 .body(body)
244 .build();
245 dialog.add_response("cancel", "Cancel");
246 dialog.add_response("confirm", action_label);
247 dialog.set_response_appearance("confirm", libadwaita::ResponseAppearance::Destructive);
248 dialog.set_default_response(Some("cancel"));
249 dialog.set_close_response("cancel");
250 let ui_for_choice = ui.clone();
251 dialog.connect_response(None, move |_, response| {
252 if response == "confirm" {
253 on_confirm(ui_for_choice.clone());
254 }
255 });
256 dialog.present(Some(&ui.window));
257}
258
259#[cfg(test)]
260mod tests {
261 use super::permanent_delete_text;
262
263 #[test]
264 fn permanent_delete_text_matches_count_and_warns() {
265 let (single, body) = permanent_delete_text(1);
266 assert_eq!(single, "Delete this item permanently?");
267 assert!(body.contains("can't be undone"));
268 assert_eq!(permanent_delete_text(3).0, "Delete 3 items permanently?");
269 }
270}