1use std::rc::Rc;
8
9use glib::clone;
10use gtk::prelude::*;
11use libadwaita::prelude::*;
12
13use crate::library::asset_object::AssetObject;
14use crate::library::state::{LibrarySortMode, LibrarySource};
15use crate::settings_window::{build_settings_window_with_parent, show_queue_inspector};
16
17use super::download::{open_local_with_default_app, spawn_video_handoff};
18use super::{
19 LibraryWindowUi, apply_timeline_ui_state, load_albums, load_source_page, load_status,
20 open_lightbox, refresh_albums_view, update_timeline_banner_if_active,
21};
22
23pub(super) fn connect_controls(ui: Rc<LibraryWindowUi>) {
24 let action_settings = gtk::gio::SimpleAction::new("settings", None);
25 action_settings.connect_activate(clone!(
26 #[strong]
27 ui,
28 move |_, _| {
29 build_settings_window_with_parent(&ui.app, ui.ctx.clone(), Some(&ui.window));
30 }
31 ));
32 ui.window.add_action(&action_settings);
33
34 let action_queue = gtk::gio::SimpleAction::new("queue", None);
35 action_queue.connect_activate(clone!(
36 #[strong]
37 ui,
38 move |_, _| {
39 show_queue_inspector(&ui.window, ui.ctx.queue_manager.clone());
40 }
41 ));
42 ui.window.add_action(&action_queue);
43
44 let action_refresh = gtk::gio::SimpleAction::new("refresh", None);
45 action_refresh.connect_activate(clone!(
46 #[strong]
47 ui,
48 move |_, _| {
49 refresh_library_surfaces(ui.clone(), true);
50 }
51 ));
52 ui.window.add_action(&action_refresh);
53
54 ui.sidebar.connection_row.connect_activated(clone!(
55 #[strong]
56 ui,
57 move |_| {
58 super::server_stats_dialog::present(ui.ctx.clone(), &ui.window);
59 }
60 ));
61 ui.sidebar.server_row.connect_activated(clone!(
62 #[strong]
63 ui,
64 move |_| {
65 super::server_stats_dialog::present(ui.ctx.clone(), &ui.window);
66 }
67 ));
68
69 ui.upload_button.connect_clicked(clone!(
70 #[strong]
71 ui,
72 move |_| {
73 let album = match ui.ctx.library_state.lock().source.clone() {
74 LibrarySource::Album { id, name }
75 | LibrarySource::AlbumLocal { id, name }
76 | LibrarySource::AlbumUnified { id, name } => Some((id, name)),
77 _ => None,
78 };
79 super::upload_picker::pick_and_upload(&ui.window, ui.ctx.clone(), album);
80 }
81 ));
82
83 ui.back_button.connect_clicked(clone!(
84 #[strong]
85 ui,
86 move |_| {
87 navigate_back(ui.clone());
88 }
89 ));
90
91 let alt_left = gtk::Shortcut::builder()
93 .trigger(>k::ShortcutTrigger::parse_string("<Alt>Left").unwrap())
94 .action(>k::CallbackAction::new(clone!(
95 #[strong]
96 ui,
97 move |_, _| {
98 if navigate_back(ui.clone()) {
99 glib::Propagation::Stop
100 } else {
101 glib::Propagation::Proceed
102 }
103 }
104 )))
105 .build();
106 let alt_left_controller = gtk::ShortcutController::new();
107 alt_left_controller.add_shortcut(alt_left);
108 ui.window.add_controller(alt_left_controller);
109
110 let f5_controller = gtk::EventControllerKey::new();
111 f5_controller.connect_key_pressed(clone!(
112 #[strong]
113 ui,
114 move |_, keyval, _, _| {
115 if keyval == gtk::gdk::Key::F5 {
116 let _ = ui
117 .window
118 .upcast_ref::<gtk::Widget>()
119 .activate_action("win.refresh", None);
120 glib::Propagation::Stop
121 } else {
122 glib::Propagation::Proceed
123 }
124 }
125 ));
126 ui.window.add_controller(f5_controller);
127
128 ui.content_stack.connect_visible_child_notify(clone!(
131 #[strong]
132 ui,
133 move |stack| {
134 let view = stack.visible_child_name();
135 match view.as_deref() {
136 Some("albums") => {
137 let model = gtk::StringList::new(&["Newest", "Name", "Most assets"]);
138 ui.sort_mode.set_model(Some(&model));
139 ui.sort_mode.set_selected(0);
140 super::albums_view::set_search_filter(&ui.albums, "");
141 }
142 Some("explore") => {
143 let model = gtk::StringList::new(&["Default"]);
144 ui.sort_mode.set_model(Some(&model));
145 ui.sort_mode.set_selected(0);
146 super::explore_view::set_people_search(&ui.explore, "");
147 }
148 _ => {
149 let model = gtk::StringList::new(&["Newest", "Filename", "File Type"]);
150 ui.sort_mode.set_model(Some(&model));
151 ui.sort_mode.set_selected(0);
152 super::albums_view::set_search_filter(&ui.albums, "");
153 super::explore_view::set_people_search(&ui.explore, "");
154 }
155 }
156 }
157 ));
158
159 ui.source_mode.connect_selected_notify(clone!(
160 #[strong]
161 ui,
162 move |dropdown| {
163 if ui.source_mode_suppressed.get() {
164 return;
165 }
166 let album_ctx = match ui.ctx.library_state.lock().source.clone() {
167 LibrarySource::Album { id, name }
168 | LibrarySource::AlbumLocal { id, name }
169 | LibrarySource::AlbumUnified { id, name } => Some((id, name)),
170 _ => None,
171 };
172 let source = match (dropdown.selected(), album_ctx) {
173 (1, Some((id, name))) => LibrarySource::AlbumLocal { id, name },
174 (1, None) => LibrarySource::LocalAll,
175 (2, Some((id, name))) => LibrarySource::AlbumUnified { id, name },
176 (2, None) => LibrarySource::Unified,
177 (_, Some((id, name))) => LibrarySource::Album { id, name },
178 (_, None) => {
179 if ui.timeline_toggle.is_active() {
180 LibrarySource::Timeline
181 } else {
182 LibrarySource::AllAssets
183 }
184 }
185 };
186
187 let request = ui.ctx.library_state.lock().navigate_to(source);
188 apply_timeline_ui_state(&ui, &request.1);
189 load_source_page(ui.clone(), request, false);
190 update_back_button(&ui);
191 }
192 ));
193
194 ui.timeline_toggle.connect_toggled(clone!(
195 #[strong]
196 ui,
197 move |toggle| {
198 if !toggle.is_sensitive() {
199 return;
200 }
201 let current = ui.ctx.library_state.lock().source.clone();
202 if !matches!(current, LibrarySource::AllAssets | LibrarySource::Timeline) {
203 toggle.set_active(false);
204 return;
205 }
206 if matches!(current, LibrarySource::Timeline) == toggle.is_active() {
207 return;
208 }
209 let next_source = if toggle.is_active() {
210 LibrarySource::Timeline
211 } else {
212 LibrarySource::AllAssets
213 };
214 let request = ui.ctx.library_state.lock().navigate_to(next_source);
215 apply_timeline_ui_state(&ui, &request.1);
216 load_source_page(ui.clone(), request, false);
217 update_back_button(&ui);
218 }
219 ));
220
221 ui.sort_mode.connect_selected_notify(clone!(
222 #[strong]
223 ui,
224 move |dropdown| {
225 let view = ui.content_stack.visible_child_name();
229 if view.as_deref() == Some("albums") {
230 let mode = match dropdown.selected() {
231 1 => super::albums_view::AlbumsSort::Name,
232 2 => super::albums_view::AlbumsSort::MostAssets,
233 _ => super::albums_view::AlbumsSort::Newest,
234 };
235 super::albums_view::set_sort_mode(&ui.albums, mode);
236 return;
237 }
238 let sort_mode = match dropdown.selected() {
239 1 => LibrarySortMode::Filename,
240 2 => LibrarySortMode::FileType,
241 _ => LibrarySortMode::NewestFirst,
242 };
243
244 let mut state = ui.ctx.library_state.lock();
245 state.apply_sort(sort_mode);
246 ui.grid
247 .model
248 .reset(&ui.ctx, &state.assets, &state.sort_mode);
249 }
250 ));
251}
252
253pub(super) fn refresh_library_surfaces(ui: Rc<LibraryWindowUi>, include_current_source: bool) {
254 load_albums(ui.clone());
255 load_status(ui.clone());
256
257 let visible = ui.content_stack.visible_child_name();
262 ui.explore.populated.set(false);
263 match visible.as_deref() {
264 Some("explore") => super::load_explore_landing(ui.clone()),
265 Some("albums") => super::refresh_albums_view(ui.clone()),
266 _ => {}
267 }
268
269 if include_current_source {
270 let request = {
271 let source = ui.ctx.library_state.lock().source.clone();
272 ui.ctx.library_state.lock().switch_source(source)
273 };
274 load_source_page(ui, request, false);
275 }
276}
277
278pub(super) fn refresh_library_after_mutation(ui: Rc<LibraryWindowUi>, prefer_current_source: bool) {
279 refresh_library_surfaces(ui, prefer_current_source);
280}
281
282pub(super) fn connect_sidebar_handlers(ui: Rc<LibraryWindowUi>) {
283 ui.sidebar.fixed_list.connect_row_selected(clone!(
285 #[strong]
286 ui,
287 move |_, row| {
288 let Some(row) = row else {
289 return;
290 };
291 let key = row.tooltip_text().unwrap_or_default();
292 ui.sidebar.albums_list.unselect_all();
293 let is_search = key.as_str() == "search";
294 ui.search_view.root.set_reveal_child(is_search);
296 match key.as_str() {
297 "photos" => sidebar_dispatch(ui.clone(), LibrarySource::Timeline),
298 "search" => {
299 ui.content_stack.set_visible_child_name("empty");
302 ui.search_view.search_entry.grab_focus();
304 }
305 "explore" => sidebar_dispatch(ui.clone(), LibrarySource::Explore),
306 "albums" => {
307 ui.album_link_row.set_visible(false);
308 if let Some(parent) = ui.album_link_row.parent() {
309 parent.set_visible(false);
310 }
311 if ui.albums.populated.get() {
312 ui.content_stack.set_visible_child_name("albums");
313 } else {
314 ui.content_stack.set_visible_child_name("loading");
315 refresh_albums_view(ui.clone());
316 }
317 }
318 _ => {}
319 }
320 auto_hide_sidebar(&ui);
321 }
322 ));
323
324 ui.sidebar.albums_list.connect_row_selected(clone!(
325 #[strong]
326 ui,
327 move |_, row| {
328 if ui.sidebar_suppressed.get() {
329 return;
330 }
331 let Some(row) = row else {
332 return;
333 };
334 let Some(tooltip) = row.tooltip_text() else {
335 return;
336 };
337 let mut parts = tooltip.splitn(2, ':');
338 let id = parts.next().unwrap_or_default().to_string();
339 let name = parts.next().unwrap_or("Album").to_string();
340 ui.sidebar.fixed_list.unselect_all();
341 ui.search_view.root.set_reveal_child(false);
342 sidebar_dispatch(ui.clone(), LibrarySource::Album { id, name });
343 auto_hide_sidebar(&ui);
344 }
345 ));
346}
347
348fn auto_hide_sidebar(ui: &Rc<LibraryWindowUi>) {
349 if ui.split.is_collapsed() {
350 ui.split.set_show_sidebar(false);
351 }
352}
353
354pub(super) fn update_back_button(ui: &Rc<LibraryWindowUi>) {
355 let can_go_back = ui.ctx.library_state.lock().can_go_back();
356 ui.back_button.set_sensitive(can_go_back);
357}
358
359pub(super) fn navigate_back(ui: Rc<LibraryWindowUi>) -> bool {
362 let request = ui.ctx.library_state.lock().navigate_back();
363 if let Some(request) = request {
364 apply_timeline_ui_state(&ui, &request.1);
365 load_source_page(ui.clone(), request, false);
366 update_back_button(&ui);
367 true
368 } else {
369 false
370 }
371}
372
373pub(super) fn sidebar_dispatch(ui: Rc<LibraryWindowUi>, source: LibrarySource) {
378 let on_albums_grid = ui.content_stack.visible_child_name().as_deref() == Some("albums");
379 if !on_albums_grid && ui.ctx.library_state.lock().source == source {
380 return;
381 }
382 let request = ui.ctx.library_state.lock().navigate_to(source);
383 apply_timeline_ui_state(&ui, &request.1);
384 load_source_page(ui.clone(), request, false);
385 update_back_button(&ui);
386}
387
388pub(super) fn connect_grid_handlers(ui: Rc<LibraryWindowUi>) {
389 let ui_for_activate = ui.clone();
390 ui.grid.canvas.set_activate_handler(move |position| {
391 let Some(item) = ui_for_activate
392 .grid
393 .model
394 .item(position)
395 .and_downcast::<AssetObject>()
396 else {
397 return;
398 };
399 let asset_id = item.property::<String>("id");
400 let filename = item.property::<String>("filename");
401 let local_path = item.property::<String>("local-path");
402 let asset_type = item.property::<String>("asset-type");
403
404 if asset_type.eq_ignore_ascii_case("VIDEO") {
406 if !local_path.is_empty() {
407 open_local_with_default_app(&local_path);
408 } else {
409 spawn_video_handoff(ui_for_activate.clone(), asset_id, filename);
410 }
411 return;
412 }
413
414 open_lightbox(ui_for_activate.clone(), position);
415 });
416
417 let scroll_pending = Rc::new(std::cell::Cell::new(false));
418 ui.grid.scrolled.vadjustment().connect_value_changed(clone!(
419 #[strong]
420 ui,
421 #[strong]
422 scroll_pending,
423 move |_adj| {
424 if scroll_pending.replace(true) {
425 return;
426 }
427 let ui = ui.clone();
428 let scroll_pending = scroll_pending.clone();
429 glib::idle_add_local_once(move || {
430 scroll_pending.set(false);
431 let adj = ui.grid.scrolled.vadjustment();
432 update_timeline_banner_if_active(&ui, &adj);
433
434 let threshold = (adj.upper() - adj.page_size()) * 0.50;
435 if adj.value() < threshold {
436 return;
437 }
438
439 let next = ui.ctx.library_state.lock().load_next_page_if_needed();
440 if let Some(request) = next {
441 load_source_page(ui.clone(), request, true);
442 }
443 });
444 }
445 ));
446}