mimick/library/
context_menu.rs1use 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(>k::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(©_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}