Skip to main content

mimick/library/
context_menu.rs

1//! Right-click context menu popover and its action handlers.
2//!
3//! Builds a GTK popover menu with per-asset actions such as download,
4//! open in lightbox, delete to trash, and copy link. Actions delegate
5//! to the shared library helpers.
6
7use std::path::PathBuf;
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::{
19    begin_download_session, finish_download_item, open_local_with_default_app, start_download,
20    track_download_item,
21};
22use super::load_texture_oriented;
23
24pub(super) fn show_asset_context_menu(
25    ui: Rc<LibraryWindowUi>,
26    parent: &impl gtk::prelude::IsA<gtk::Widget>,
27    position: u32,
28    x: f64,
29    y: f64,
30) {
31    let Some(item) = ui.grid.model.item(position).and_downcast::<AssetObject>() else {
32        return;
33    };
34    let asset_id = item.property::<String>("id");
35    let remote_id = item.property::<String>("remote-id");
36    let local_path = item.property::<String>("local-path");
37    let filename = item.property::<String>("filename");
38    let asset_type = item.property::<String>("asset-type");
39    let is_image = asset_type.eq_ignore_ascii_case("IMAGE");
40    let can_download = !remote_id.is_empty() && !asset_id.starts_with(LOCAL_ID_PREFIX);
41    let can_open = can_download || !local_path.is_empty();
42
43    let popover = gtk::Popover::builder()
44        .has_arrow(true)
45        .autohide(true)
46        .build();
47    popover.set_parent(parent);
48    popover.set_pointing_to(Some(&gtk::gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
49
50    let content = gtk::Box::builder()
51        .orientation(gtk::Orientation::Vertical)
52        .spacing(4)
53        .margin_top(6)
54        .margin_bottom(6)
55        .margin_start(6)
56        .margin_end(6)
57        .build();
58
59    if is_image {
60        let copy_btn = gtk::Button::builder()
61            .label("Copy")
62            .halign(gtk::Align::Fill)
63            .build();
64        copy_btn.connect_clicked(clone!(
65            #[strong]
66            ui,
67            #[strong]
68            popover,
69            #[strong]
70            asset_id,
71            #[strong]
72            remote_id,
73            #[strong]
74            local_path,
75            #[strong]
76            filename,
77            move |_| {
78                popover.popdown();
79                copy_asset_to_clipboard(
80                    ui.clone(),
81                    asset_id.clone(),
82                    remote_id.clone(),
83                    local_path.clone(),
84                    filename.clone(),
85                );
86            }
87        ));
88        content.append(&copy_btn);
89    }
90
91    if can_download {
92        let download_btn = gtk::Button::builder()
93            .label("Download")
94            .halign(gtk::Align::Fill)
95            .build();
96        download_btn.connect_clicked(clone!(
97            #[strong]
98            ui,
99            #[strong]
100            popover,
101            #[strong]
102            remote_id,
103            #[strong]
104            filename,
105            move |_| {
106                popover.popdown();
107                start_download(ui.clone(), remote_id.clone(), filename.clone());
108            }
109        ));
110        content.append(&download_btn);
111    }
112
113    if can_open {
114        let open_btn = gtk::Button::builder()
115            .label("Open In")
116            .halign(gtk::Align::Fill)
117            .build();
118        open_btn.connect_clicked(clone!(
119            #[strong]
120            ui,
121            #[strong]
122            popover,
123            #[strong]
124            asset_id,
125            #[strong]
126            remote_id,
127            #[strong]
128            local_path,
129            #[strong]
130            filename,
131            move |_| {
132                popover.popdown();
133                open_asset_in_default_app(
134                    ui.clone(),
135                    asset_id.clone(),
136                    remote_id.clone(),
137                    local_path.clone(),
138                    filename.clone(),
139                );
140            }
141        ));
142        content.append(&open_btn);
143    }
144
145    popover.set_child(Some(&content));
146    popover.popup();
147}
148
149fn copy_asset_to_clipboard(
150    ui: Rc<LibraryWindowUi>,
151    asset_id: String,
152    remote_id: String,
153    local_path: String,
154    filename: String,
155) {
156    glib::MainContext::default().spawn_local(async move {
157        let path =
158            match ensure_original_asset_path(&ui, &asset_id, &remote_id, &local_path, &filename)
159                .await
160            {
161                Ok(path) => path,
162                Err(err) => {
163                    show_alert_dialog(&ui, "Copy Failed", &err);
164                    return;
165                }
166            };
167        let Some(texture) = load_texture_oriented(&path).await else {
168            show_alert_dialog(&ui, "Copy Failed", "Could not decode the original image.");
169            return;
170        };
171        if let Some(display) = gdk4::Display::default() {
172            display.clipboard().set_texture(&texture);
173        }
174    });
175}
176
177fn open_asset_in_default_app(
178    ui: Rc<LibraryWindowUi>,
179    asset_id: String,
180    remote_id: String,
181    local_path: String,
182    filename: String,
183) {
184    glib::MainContext::default().spawn_local(async move {
185        let path =
186            match ensure_original_asset_path(&ui, &asset_id, &remote_id, &local_path, &filename)
187                .await
188            {
189                Ok(path) => path,
190                Err(err) => {
191                    show_alert_dialog(&ui, "Open Failed", &err);
192                    return;
193                }
194            };
195        open_local_with_default_app(&path.display().to_string());
196    });
197}
198
199pub(super) async fn ensure_original_asset_path(
200    ui: &LibraryWindowUi,
201    asset_id: &str,
202    remote_id: &str,
203    local_path: &str,
204    filename: &str,
205) -> Result<PathBuf, String> {
206    if !local_path.is_empty() {
207        return Ok(PathBuf::from(local_path));
208    }
209    let remote_asset_id = if !remote_id.is_empty() {
210        remote_id
211    } else {
212        asset_id
213    };
214    let cache_dir = crate::profile::cache_dir()
215        .ok_or_else(|| "Could not locate a cache directory.".to_string())?
216        .join("open-in");
217    let _ = std::fs::create_dir_all(&cache_dir);
218    let safe_name =
219        crate::sanitize::safe_filename(filename).unwrap_or_else(|| asset_id.to_string());
220    let path = cache_dir.join(&safe_name);
221    if path.exists() {
222        return Ok(path);
223    }
224    begin_download_session(&ui.ctx, filename.to_string());
225    let progress = track_download_item(
226        &ui.ctx,
227        remote_asset_id.to_string(),
228        Some(filename.to_string()),
229        None,
230    );
231    let result = ui
232        .ctx
233        .api_client
234        .download_original_to_file(remote_asset_id, &path, Some(progress))
235        .await;
236    finish_download_item(&ui.ctx, remote_asset_id);
237    result.map(|_| path).map_err(|err| err.to_string())
238}
239
240pub(super) fn show_alert_dialog(ui: &LibraryWindowUi, heading: &str, body: &str) {
241    let alert = libadwaita::AlertDialog::builder()
242        .heading(heading)
243        .body(body)
244        .build();
245    alert.add_response("ok", "OK");
246    alert.present(Some(&ui.window));
247}