1use std::{
2 rc::Rc,
3 sync::{Arc, Mutex},
4};
5
6use slint::Model;
7
8use crate::{
9 item_sort_list::{timestamp_to_string, FileItem, Format, ItemList},
10 main_window,
11 misc::image_cache,
12};
13
14use super::helper;
15
16pub struct ItemsController {
17 item_list: Arc<Mutex<ItemList>>,
18 list_model: Rc<slint::VecModel<main_window::ListItem>>,
19 similar_items_model: Rc<slint::VecModel<main_window::SortItem>>,
20 image_cache: image_cache::ImageCache,
21}
22
23impl ItemsController {
24 pub fn new(item_list: Arc<Mutex<ItemList>>) -> Self {
26 let mut image_cache = image_cache::ImageCache::new();
27 image_cache.restrict_size(1600, 1000);
28
29 Self {
30 item_list,
31 list_model: Rc::new(slint::VecModel::<main_window::ListItem>::default()),
32 similar_items_model: Rc::new(slint::VecModel::<main_window::SortItem>::default()),
33 image_cache,
34 }
35 }
36
37 pub fn get_list_model(&self) -> Rc<slint::VecModel<main_window::ListItem>> {
39 self.list_model.clone()
40 }
41
42 pub fn get_similar_items_model(&self) -> Rc<slint::VecModel<main_window::SortItem>> {
44 self.similar_items_model.clone()
45 }
46
47 pub fn clear_list(&mut self) {
49 helper::clear_model(self.list_model.clone());
50 }
51
52 pub fn clear_similar_items(&mut self) {
54 helper::clear_model(self.similar_items_model.clone());
55 }
56
57 pub fn find_index_by_path(&self, path: &str) -> Option<usize> {
59 let item_list = self.item_list.lock().unwrap();
60 item_list.index_of_path(path)
61 }
62
63 pub fn selected_list_item(
66 &mut self,
67 list_model_index: usize,
68 window: slint::Weak<main_window::ImageSieve>,
69 ) {
70 if list_model_index >= self.list_model.row_count() {
71 return;
72 }
73 {
74 self.clear_similar_items();
76
77 let items_index = self
78 .list_model
79 .row_data(list_model_index)
80 .unwrap()
81 .local_index as usize;
82 let item_list = self.item_list.lock().unwrap();
83 let similars = item_list.items[items_index].get_similars();
84
85 self.image_cache.purge();
87
88 let item = &item_list.items[items_index];
90 let image = self.get_item_image(
91 item,
92 0,
93 items_index as i32,
94 true,
95 !similars.is_empty(),
96 window.clone(),
97 );
98 let sort_image = sort_item_from_file_item(item, &item_list, image);
99 self.similar_items_model.push(sort_image);
100
101 let mut model_index = 1;
103 for image_index in similars {
104 let item = &item_list.items[*image_index];
105 let image = self.get_item_image(
106 item,
107 model_index,
108 items_index as i32,
109 false,
110 !similars.is_empty(),
111 window.clone(),
112 );
113 let sort_image = sort_item_from_file_item(item, &item_list, image);
114 self.similar_items_model.push(sort_image);
115 model_index += 1;
116 }
117 }
118
119 window
121 .unwrap()
122 .set_current_image(self.similar_items_model.row_data(0).unwrap());
123 window.unwrap().invoke_current_image_changed();
124
125 self.prefetch_images(list_model_index);
127 }
128
129 pub fn set_take_over(&mut self, local_index: i32, take_over: bool) -> slint::SharedString {
131 let description = {
132 let mut item_list = self.item_list.lock().unwrap();
134 item_list.items[local_index as usize].set_take_over(take_over);
135 sort_item_description(&item_list.items[local_index as usize], &item_list)
136 };
137 self.update_list_model();
139 for count in 0..self.similar_items_model.row_count() {
141 let mut item: main_window::SortItem = self.similar_items_model.row_data(count).unwrap();
142 if item.local_index == local_index {
143 item.take_over = take_over;
144 item.text = description.clone();
145 self.similar_items_model.set_row_data(count, item);
146 break;
147 }
148 }
149 description
150 }
151
152 pub fn update_list_model(&mut self) -> bool {
155 let item_list = self.item_list.lock().unwrap();
156 for count in 0..self.list_model.row_count() {
157 let mut list_item = self.list_model.row_data(count).unwrap();
158 let file_item = &item_list.items[list_item.local_index as usize];
159 list_item.text = list_item_title(file_item, &item_list);
160 self.list_model.set_row_data(count, list_item);
161 }
162 !item_list.items.is_empty()
163 }
164
165 pub fn populate_list_model(&mut self, filters: &main_window::Filters) -> usize {
167 self.clear_list();
168
169 let item_list = self.item_list.lock().unwrap();
170 let mut filtered_list: Vec<&FileItem> = item_list
171 .items
172 .iter()
173 .filter(|item| filter_file_items(item, filters))
174 .collect();
175 filtered_list.sort_unstable_by(|a, b| compare_file_items(a, b, filters));
176 if filters.direction == "Desc" {
177 filtered_list.reverse();
178 }
179 let list_len = filtered_list.len();
180 for image in filtered_list {
181 let list_item = list_item_from_file_item(image, &item_list);
182 self.list_model.push(list_item);
183 }
184 list_len
185 }
186
187 pub fn get_date_string(&self, local_index: i32) -> slint::SharedString {
189 let item_list = self.item_list.lock().unwrap();
190 let item = &item_list.items[local_index as usize];
191 slint::SharedString::from(timestamp_to_string(item.get_timestamp(), Format::Date))
192 }
193
194 fn get_item_image(
198 &self,
199 item: &FileItem,
200 model_index: usize,
201 current_item_local_index: i32,
202 is_current_image: bool,
203 has_similars: bool,
204 window_weak: slint::Weak<main_window::ImageSieve>,
205 ) -> slint::Image {
206 let image = self.image_cache.get(item);
207 if let Some(image) = image {
208 image
209 } else {
210 let f: image_cache::DoneCallback = Box::new(move |image_buffer| {
211 window_weak
212 .clone()
213 .upgrade_in_event_loop(move |handle| {
214 if handle.get_current_image().local_index == current_item_local_index {
216 let mut row_data = handle
217 .get_similar_images_model()
218 .row_data(model_index)
219 .unwrap();
220 if has_similars {
221 row_data.image =
222 crate::misc::images::get_slint_image(&image_buffer);
223 handle
224 .get_similar_images_model()
225 .set_row_data(model_index, row_data);
226 }
227 if is_current_image {
229 let mut current_image = handle.get_current_image();
230 current_image.image =
231 crate::misc::images::get_slint_image(&image_buffer);
232 handle.set_current_image(current_image);
233 handle.invoke_current_image_changed();
234 }
235 }
236 })
237 .unwrap()
238 });
239 self.image_cache.load(
240 item,
241 if is_current_image {
242 image_cache::Purpose::CurrentImage
243 } else {
244 image_cache::Purpose::SimilarImage
245 },
246 Some(f),
247 );
248 self.image_cache.get_waiting()
249 }
250 }
251
252 fn prefetch_images(&self, list_model_index: usize) {
254 for i in list_model_index + 1..list_model_index + 3 {
256 if i < self.list_model.row_count() {
257 let item_list = self.item_list.lock().unwrap();
258 let list_item = &self.list_model.row_data(i).unwrap();
259 let file_item = &item_list.items[list_item.local_index as usize];
260 if file_item.is_image() {
261 self.image_cache
262 .load(file_item, image_cache::Purpose::Prefetch, None);
263 }
264 }
265 }
266 }
267}
268
269fn filter_file_items(file_item: &FileItem, filters: &main_window::Filters) -> bool {
271 let mut visible = true;
272 if !filters.images && (file_item.is_image() || file_item.is_raw_image()) {
273 visible = false;
274 }
275 if !filters.videos && file_item.is_video() {
276 visible = false;
277 }
278 if !filters.sorted_out && !file_item.get_take_over() {
279 visible = false;
280 }
281 if filters.only_similars && file_item.get_similars().is_empty() {
282 visible = false;
283 }
284 visible
285}
286
287fn compare_file_items(
289 a: &FileItem,
290 b: &FileItem,
291 filters: &main_window::Filters,
292) -> std::cmp::Ordering {
293 match filters.sort_by.as_str() {
294 "Date" => a.cmp(b),
295 "Name" => a.path.cmp(&b.path),
296 "Type" => {
297 if a.is_image() && b.is_image() {
298 a.cmp(b)
299 } else if a.is_image() && b.is_video() {
300 std::cmp::Ordering::Less
301 } else {
302 std::cmp::Ordering::Greater
303 }
304 }
305 "Size" => a.get_size().cmp(&b.get_size()),
306 _ => panic!("Unknown sort by type"),
307 }
308}
309
310fn sort_item_from_file_item(
312 file_item: &FileItem,
313 item_list: &ItemList,
314 image: slint::Image,
315) -> main_window::SortItem {
316 main_window::SortItem {
317 text: sort_item_description(file_item, item_list),
318 image,
319 take_over: file_item.get_take_over(),
320 local_index: item_list.index_of_item(file_item).unwrap() as i32,
321 }
322}
323
324fn sort_item_description(file_item: &FileItem, item_list: &ItemList) -> slint::SharedString {
326 let mut description = format!("{}", file_item);
327 if let Some(event) = item_list.get_event(file_item) {
328 description = description + ", 📅 " + &event.name;
329 }
330 slint::SharedString::from(description)
331}
332
333fn list_item_title(file_item: &FileItem, item_list: &ItemList) -> slint::SharedString {
335 let mut title = file_item.get_item_string(&item_list.path);
336 if item_list.get_event(file_item).is_some() {
337 title = String::from("📅 ") + &title;
338 }
339 slint::SharedString::from(title)
340}
341
342fn list_item_from_file_item(file_item: &FileItem, item_list: &ItemList) -> main_window::ListItem {
344 main_window::ListItem {
345 text: list_item_title(file_item, item_list),
346 local_index: item_list.index_of_item(file_item).unwrap() as i32,
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use crate::main_window::ImageSieve;
353 use rusty_fork::rusty_fork_test;
354 use slint::{ComponentHandle, SharedString};
355
356 use super::*;
357
358 fn build_filters() -> main_window::Filters {
359 main_window::Filters {
360 images: true,
361 videos: true,
362 sorted_out: true,
363 only_similars: false,
364 sort_by: SharedString::from("Date"),
365 direction: SharedString::from("Asc"),
366 }
367 }
368
369 #[test]
370 fn test_populate() {
371 let item_list = Arc::new(Mutex::new(ItemList::new()));
372 let mut items_controller = ItemsController::new(item_list.clone());
373 let mut filters = build_filters();
374 {
375 let mut item_list = item_list.lock().unwrap();
376 item_list.items.push(FileItem::dummy("test2.mov", 1, true));
377 let mut file_item = FileItem::dummy("test1.jpg", 0, false);
378 file_item.add_similar_range(&(1..2));
379 item_list.items.push(file_item);
380 }
381 items_controller.populate_list_model(&filters);
382 let list_model = items_controller.get_list_model();
383 assert_eq!(list_model.row_count(), 2);
384 assert_eq!(list_model.row_data(0).unwrap().local_index, 1);
385 assert_eq!(list_model.row_data(0).unwrap().text, "🔀 📷 🗑 test1.jpg");
386 assert_eq!(list_model.row_data(1).unwrap().local_index, 0);
387 assert_eq!(list_model.row_data(1).unwrap().text, "📹 test2.mov");
388 assert_eq!(Some(1), items_controller.find_index_by_path("test1.jpg"));
389
390 filters.direction = SharedString::from("Desc");
391 items_controller.populate_list_model(&filters);
392 assert_eq!(list_model.row_count(), 2);
393 assert_eq!(list_model.row_data(1).unwrap().local_index, 1);
394 assert_eq!(list_model.row_data(1).unwrap().text, "🔀 📷 🗑 test1.jpg");
395 assert_eq!(list_model.row_data(0).unwrap().local_index, 0);
396 assert_eq!(list_model.row_data(0).unwrap().text, "📹 test2.mov");
397
398 filters.images = false;
399 items_controller.populate_list_model(&filters);
400 assert_eq!(list_model.row_count(), 1);
401 assert_eq!(list_model.row_data(0).unwrap().local_index, 0);
402 assert_eq!(list_model.row_data(0).unwrap().text, "📹 test2.mov");
403
404 filters.images = true;
405 filters.videos = false;
406 items_controller.populate_list_model(&filters);
407 assert_eq!(list_model.row_count(), 1);
408 assert_eq!(list_model.row_data(0).unwrap().local_index, 1);
409 assert_eq!(list_model.row_data(0).unwrap().text, "🔀 📷 🗑 test1.jpg");
410
411 filters.videos = true;
412 filters.sorted_out = false;
413 items_controller.populate_list_model(&filters);
414 assert_eq!(list_model.row_count(), 1);
415 assert_eq!(list_model.row_data(0).unwrap().local_index, 0);
416 assert_eq!(list_model.row_data(0).unwrap().text, "📹 test2.mov");
417
418 filters.sorted_out = true;
419 filters.only_similars = true;
420 items_controller.populate_list_model(&filters);
421 assert_eq!(list_model.row_count(), 1);
422 assert_eq!(list_model.row_data(0).unwrap().local_index, 1);
423 assert_eq!(list_model.row_data(0).unwrap().text, "🔀 📷 🗑 test1.jpg");
424
425 items_controller.clear_list();
426 assert_eq!(items_controller.get_list_model().row_count(), 0);
427 }
428
429 rusty_fork_test! {
430 #[test]
431 fn test_take_over() {
432 let item_list = Arc::new(Mutex::new(ItemList::new()));
433 let mut items_controller = ItemsController::new(item_list.clone());
434 let window = ImageSieve::new().unwrap();
435 let window_weak = window.as_weak();
436 let filters = build_filters();
437 {
438 let mut item_list = item_list.lock().unwrap();
439 item_list.items.push(FileItem::dummy("test2.mov", 1, true));
440 let mut file_item = FileItem::dummy("test1.jpg", 0, false);
441 file_item.add_similar_range(&(0..1));
442 item_list.items.push(file_item);
443 }
444 items_controller.populate_list_model(&filters);
445 items_controller.selected_list_item(1, window_weak);
446
447 items_controller.set_take_over(0, false);
448 {
449 let item_list = item_list.lock().unwrap();
450 assert!(!item_list.items[0].get_take_over());
451 }
452 let list_model = items_controller.get_list_model();
453 let similar_items_model = items_controller.get_similar_items_model();
454 assert_eq!(list_model.row_data(1).unwrap().text, "📹 🗑 test2.mov");
455 assert!(!similar_items_model.row_data(0).unwrap().take_over);
456
457 items_controller.set_take_over(0, true);
458 {
459 let item_list = item_list.lock().unwrap();
460 assert!(item_list.items[0].get_take_over());
461 }
462 assert_eq!(list_model.row_data(1).unwrap().text, "📹 test2.mov");
463 assert!(window.get_current_image().take_over);
464 assert!(similar_items_model.row_data(0).unwrap().take_over);
465 }
466 }
467
468 #[test]
469 fn test_select_item() {
470 let item_list = Arc::new(Mutex::new(ItemList::new()));
471 let mut items_controller = ItemsController::new(item_list.clone());
472 let window = ImageSieve::new().unwrap();
473 let window_weak = window.as_weak();
474 let filters = build_filters();
475 {
476 let mut item_list = item_list.lock().unwrap();
477 item_list.items.push(FileItem::dummy("test2.mov", 1, true));
478 let mut file_item = FileItem::dummy("test1.jpg", 0, false);
479 file_item.add_similar_range(&(0..1));
480 item_list.items.push(file_item);
481 }
482 items_controller.populate_list_model(&filters);
483
484 let similar_items_model = items_controller.get_similar_items_model();
485
486 items_controller.selected_list_item(0, window_weak.clone());
487 assert_eq!(similar_items_model.row_count(), 2);
488 assert_eq!(
489 similar_items_model.row_data(0).unwrap().text,
490 "🔀 📷 🗑 test1.jpg - 1970-01-01 00:00:00, 0 KB"
491 );
492 assert_eq!(
493 similar_items_model.row_data(1).unwrap().text,
494 "📹 test2.mov - 1970-01-01 00:00:01, 0 KB"
495 );
496 assert_eq!(
497 similar_items_model.row_data(0).unwrap().image.size().width as i32,
498 16
499 );
500 assert_eq!(window.get_current_image().image.size().height as i32, 16);
501 assert_eq!(window.get_current_image().local_index, 1);
502
503 items_controller.selected_list_item(1, window_weak);
504 assert_eq!(similar_items_model.row_count(), 1);
505 assert_eq!(window.get_current_image().local_index, 0);
506 }
507
508 #[test]
509 fn test_update_list() {
510 let item_list = Arc::new(Mutex::new(ItemList::new()));
511 let mut items_controller = ItemsController::new(item_list.clone());
512 assert!(!items_controller.update_list_model());
513 let filters = build_filters();
514 {
515 let mut item_list = item_list.lock().unwrap();
516 item_list.items.push(FileItem::dummy("test2.mov", 1, true));
517 let mut file_item = FileItem::dummy("test1.jpg", 0, false);
518 file_item.add_similar_range(&(1..2));
519 item_list.items.push(file_item);
520 }
521 items_controller.populate_list_model(&filters);
522
523 {
524 let mut item_list = item_list.lock().unwrap();
525 item_list.events.push(crate::item_sort_list::Event::new(
526 "Test",
527 "1970-01-01",
528 "1970-01-01",
529 ));
530 }
531 assert!(items_controller.update_list_model());
532 let list_model = items_controller.get_list_model();
533 assert_eq!(list_model.row_count(), 2);
534 assert_eq!(list_model.row_data(0).unwrap().text, "📅 🔀 📷 🗑 test1.jpg");
535 assert_eq!(list_model.row_data(1).unwrap().text, "📅 📹 test2.mov");
536 }
537}