mimick/settings_window/
queue_inspector.rs1use std::path::Path;
9use std::sync::Arc;
10
11use adw::prelude::*;
12use glib::clone;
13use gtk::prelude::*;
14use gtk::{Box, Button, ListBox, Orientation, ScrolledWindow};
15use libadwaita as adw;
16
17use crate::queue_manager::QueueManager;
18use crate::state_manager::QueueEvent;
19
20pub fn show_queue_inspector(
22 parent: &impl gtk::prelude::IsA<gtk::Window>,
23 queue_manager: Arc<QueueManager>,
24) {
25 let dialog = adw::Window::builder()
26 .transient_for(parent)
27 .modal(true)
28 .title("Queue Inspector")
29 .default_width(900)
30 .default_height(680)
31 .width_request(360)
32 .height_request(480)
33 .build();
34
35 let header = adw::HeaderBar::builder().show_title(true).build();
36
37 let content = Box::builder()
38 .orientation(Orientation::Vertical)
39 .spacing(12)
40 .margin_top(12)
41 .margin_bottom(12)
42 .margin_start(12)
43 .margin_end(12)
44 .build();
45
46 let main_scroll = ScrolledWindow::builder()
47 .hscrollbar_policy(gtk::PolicyType::Never)
48 .child(&content)
49 .build();
50
51 let toolbar = adw::ToolbarView::builder().build();
52 toolbar.add_top_bar(&header);
53 toolbar.set_content(Some(&main_scroll));
54 dialog.set_content(Some(&toolbar));
55
56 let actions = Box::builder()
57 .orientation(Orientation::Horizontal)
58 .spacing(8)
59 .halign(gtk::Align::End)
60 .build();
61 content.append(&actions);
62
63 let retry_all_btn = Button::builder().label("Retry All Failed").build();
64 let clear_failed_btn = Button::builder().label("Clear Failed Queue").build();
65 actions.append(&retry_all_btn);
66 actions.append(&clear_failed_btn);
67
68 let failed_group = adw::PreferencesGroup::builder()
69 .title("Failed Retry Queue")
70 .build();
71 content.append(&failed_group);
72
73 let events_group = adw::PreferencesGroup::builder()
74 .title("Recent Queue Activity")
75 .build();
76 content.append(&events_group);
77
78 let events_list = ListBox::builder()
79 .selection_mode(gtk::SelectionMode::None)
80 .css_classes(vec!["boxed-list".to_string()])
81 .build();
82 events_group.add(&events_list);
83
84 refresh_inspector(&failed_group, &events_list, &queue_manager);
86
87 let qm_retry_all = queue_manager.clone();
89 let fg_retry = failed_group.clone();
90 let el_retry = events_list.clone();
91 retry_all_btn.connect_clicked(move |btn| {
92 btn.set_sensitive(false);
93 let qm = qm_retry_all.clone();
94 let fg = fg_retry.clone();
95 let el = el_retry.clone();
96 glib::MainContext::default().spawn_local(async move {
97 let _ = qm.retry_all_failed().await;
98 refresh_inspector(&fg, &el, &qm);
99 });
100 });
101
102 let qm_clear = queue_manager.clone();
103 let fg_clear = failed_group.clone();
104 let el_clear = events_list.clone();
105 clear_failed_btn.connect_clicked(move |_| {
106 let _ = qm_clear.clear_failed();
107 refresh_inspector(&fg_clear, &el_clear, &qm_clear);
108 });
109
110 let prev_failed_count = std::cell::Cell::new(queue_manager.failed_tasks().len());
112 let prev_event_count = std::cell::Cell::new(queue_manager.recent_events().len());
113 let qm_tick = queue_manager.clone();
114 glib::timeout_add_local(
115 std::time::Duration::from_secs(1),
116 clone!(
117 #[weak]
118 failed_group,
119 #[weak]
120 events_list,
121 #[upgrade_or]
122 glib::ControlFlow::Break,
123 move || {
124 let failed = qm_tick.failed_tasks();
125 let events = qm_tick.recent_events();
126 let changed = failed.len() != prev_failed_count.get()
127 || events.len() != prev_event_count.get()
128 || has_status_change(&events, &failed);
129 if changed {
130 prev_failed_count.set(failed.len());
131 prev_event_count.set(events.len());
132 refresh_inspector(&failed_group, &events_list, &qm_tick);
133 }
134 glib::ControlFlow::Continue
135 }
136 ),
137 );
138
139 let bp = adw::Breakpoint::new(
140 adw::BreakpointCondition::parse("max-width: 500sp").expect("valid breakpoint condition"),
141 );
142 bp.add_setter(&retry_all_btn, "label", Some(&"Retry All".to_value()));
143 bp.add_setter(&clear_failed_btn, "label", Some(&"Clear".to_value()));
144
145 dialog.add_breakpoint(bp);
146
147 dialog.present();
148}
149
150fn has_status_change(events: &[QueueEvent], failed: &[crate::queue_manager::FileTask]) -> bool {
152 let latest_ts = events.first().map(|e| e.timestamp).unwrap_or(0.0);
154 let failed_sig: u64 = failed.iter().map(|t| t.path.len() as u64).sum();
155 let sig = (latest_ts * 1000.0) as u64 ^ failed_sig;
157 sig != 0
161}
162
163fn refresh_inspector(
165 failed_group: &adw::PreferencesGroup,
166 events_list: &ListBox,
167 queue_manager: &Arc<QueueManager>,
168) {
169 let failed_tasks = queue_manager.failed_tasks();
170 let events = queue_manager.recent_events();
171 rebuild_failed_rows(failed_group, &failed_tasks, queue_manager);
172 rebuild_event_rows(events_list, &events);
173}
174
175fn clear_preferences_group(group: &adw::PreferencesGroup) {
177 while let Some(child) = group
178 .first_child()
179 .and_then(|c| c.last_child())
180 .and_then(|c| c.last_child())
181 .and_then(|c| c.first_child())
182 {
183 if child.downcast_ref::<adw::ActionRow>().is_some() {
184 group.remove(&child);
185 } else {
186 break;
187 }
188 }
189}
190
191fn rebuild_failed_rows(
193 group: &adw::PreferencesGroup,
194 tasks: &[crate::queue_manager::FileTask],
195 queue_manager: &Arc<QueueManager>,
196) {
197 clear_preferences_group(group);
198
199 if tasks.is_empty() {
200 group.add(
201 &adw::ActionRow::builder()
202 .title("No failed items")
203 .subtitle("The retry queue is currently empty.")
204 .build(),
205 );
206 return;
207 }
208
209 for task in tasks {
210 let row = adw::ActionRow::builder()
211 .title(
212 Path::new(&task.path)
213 .file_name()
214 .and_then(|name| name.to_str())
215 .unwrap_or(task.path.as_str()),
216 )
217 .subtitle(&task.path)
218 .title_lines(0)
219 .subtitle_lines(3)
220 .build();
221 let retry_btn = Button::builder().label("Retry").build();
222 let task_path = task.path.clone();
223 let qm = queue_manager.clone();
224 let group_ref = group.clone();
225 retry_btn.connect_clicked(move |btn| {
226 btn.set_sensitive(false);
227 btn.set_label("Retrying\u{2026}");
228 let qm = qm.clone();
229 let path = task_path.clone();
230 let group_ref = group_ref.clone();
231 glib::MainContext::default().spawn_local(async move {
232 let _ = qm.retry_failed_path(&path).await;
233 let refreshed = qm.failed_tasks();
234 rebuild_failed_rows(&group_ref, &refreshed, &qm);
235 });
236 });
237 row.add_suffix(&retry_btn);
238 group.add(&row);
239 }
240}
241
242fn rebuild_event_rows(list: &ListBox, events: &[QueueEvent]) {
244 list.remove_all();
245
246 for event in events {
247 let row = adw::ActionRow::builder()
248 .title(
249 Path::new(&event.path)
250 .file_name()
251 .and_then(|name| name.to_str())
252 .unwrap_or(event.path.as_str()),
253 )
254 .subtitle(format!(
255 "{} | attempts={}{}",
256 event.status,
257 event.attempts,
258 event
259 .detail
260 .as_ref()
261 .map(|detail| format!(" | {}", detail))
262 .unwrap_or_default()
263 ))
264 .title_lines(0)
265 .subtitle_lines(4)
266 .build();
267
268 list.append(&row);
269 }
270}
271
272pub(super) fn show_about_dialog(parent: &impl gtk::prelude::IsA<gtk::Widget>) {
274 let about = adw::AboutDialog::builder()
275 .application_name("Mimick")
276 .application_icon("dev.nicx.mimick")
277 .version(env!("CARGO_PKG_VERSION"))
278 .developer_name("Nick Cardoso")
279 .website("https://github.com/nicx17/mimick")
280 .issue_url("https://github.com/nicx17/mimick/issues")
281 .license_type(gtk::License::Gpl30)
282 .build();
283
284 about.add_credit_section(
285 Some("Icon Design"),
286 &["Round Icons https://unsplash.com/illustrations/a-white-and-orange-flower-on-a-white-background-IkQ_WrJzZOM"],
287 );
288
289 let third_party =
290 glib::markup_escape_text(include_str!("../../THIRD_PARTY_LICENSES_SUMMARY.txt"));
291 about.add_legal_section(
292 "Third-party Licenses",
293 None,
294 gtk::License::Custom,
295 Some(&third_party),
296 );
297
298 about.present(Some(parent));
299}