diff --git a/czkawka_core/src/empty_folder.rs b/czkawka_core/src/empty_folder.rs index 89c188a..bb13f88 100644 --- a/czkawka_core/src/empty_folder.rs +++ b/czkawka_core/src/empty_folder.rs @@ -26,6 +26,12 @@ pub struct FolderEntry { pub modified_date: u64, } +impl FolderEntry { + pub fn get_modified_date(&self) -> u64 { + self.modified_date + } +} + pub struct EmptyFolder { common_data: CommonToolData, information: Info, diff --git a/krokiet/src/common.rs b/krokiet/src/common.rs index adf777a..8f29d84 100644 --- a/krokiet/src/common.rs +++ b/krokiet/src/common.rs @@ -4,7 +4,7 @@ use crate::{CurrentTab, ExcludedDirectoriesModel, IncludedDirectoriesModel, Main use slint::{ModelRc, SharedString, VecModel}; // Remember to match updated this according to ui/main_lists.slint and connect_scan.rs files -pub fn get_path_idx(active_tab: CurrentTab) -> usize { +pub fn get_str_path_idx(active_tab: CurrentTab) -> usize { match active_tab { CurrentTab::EmptyFolders => 1, CurrentTab::EmptyFiles => 1, @@ -12,7 +12,7 @@ pub fn get_path_idx(active_tab: CurrentTab) -> usize { CurrentTab::Settings => panic!("Button should be disabled"), } } -pub fn get_name_idx(active_tab: CurrentTab) -> usize { +pub fn get_str_name_idx(active_tab: CurrentTab) -> usize { match active_tab { CurrentTab::EmptyFolders => 0, CurrentTab::EmptyFiles => 0, @@ -20,6 +20,39 @@ pub fn get_name_idx(active_tab: CurrentTab) -> usize { CurrentTab::Settings => panic!("Button should be disabled"), } } + +pub fn get_int_modification_date_idx(active_tab: CurrentTab) -> usize { + match active_tab { + CurrentTab::EmptyFiles => 0, + CurrentTab::SimilarImages => 0, + CurrentTab::EmptyFolders => 0, + CurrentTab::Settings => panic!("Button should be disabled"), + } +} +pub fn get_int_size_idx(active_tab: CurrentTab) -> usize { + match active_tab { + CurrentTab::EmptyFiles => 2, + CurrentTab::SimilarImages => 2, + CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::EmptyFolders => panic!("Unable to get size from this tab"), + } +} + +pub fn get_width_idx(active_tab: CurrentTab) -> usize { + match active_tab { + CurrentTab::SimilarImages => 4, + CurrentTab::Settings => panic!("Button should be disabled"), + _ => panic!("Unable to get height from this tab"), + } +} +pub fn get_height_idx(active_tab: CurrentTab) -> usize { + match active_tab { + CurrentTab::SimilarImages => 5, + CurrentTab::Settings => panic!("Button should be disabled"), + _ => panic!("Unable to get height from this tab"), + } +} + pub fn get_is_header_mode(active_tab: CurrentTab) -> bool { match active_tab { CurrentTab::EmptyFolders | CurrentTab::EmptyFiles => false, @@ -99,3 +132,56 @@ pub fn create_excluded_directories_model_from_pathbuf(items: &[PathBuf]) -> Mode pub fn create_vec_model_from_vec_string(items: Vec) -> VecModel { VecModel::from(items.into_iter().map(SharedString::from).collect::>()) } + +// Workaround for https://github.com/slint-ui/slint/discussions/4596 +// Currently there is no way to save u64 in slint, so we need to split it into two i32 +pub fn split_u64_into_i32s(value: u64) -> (i32, i32) { + let part1: i32 = (value >> 32) as i32; + let part2: i32 = value as i32; + (part1, part2) +} +pub fn connect_i32_into_u64(part1: i32, part2: i32) -> u64 { + ((part1 as u64) << 32) | (part2 as u64 & 0xFFFFFFFF) +} + +#[cfg(test)] +mod test { + use crate::common::split_u64_into_i32s; + + #[test] + fn test_split_u64_into_i32s_small() { + let value = 1; + let (part1, part2) = split_u64_into_i32s(value); + assert_eq!(part1, 0); + assert_eq!(part2, 1); + } + #[test] + fn test_split_u64_into_i32s_big() { + let value = u64::MAX; + let (part1, part2) = split_u64_into_i32s(value); + assert_eq!(part1, -1); + assert_eq!(part2, -1); + } + #[test] + fn test_connect_i32_into_u64_small() { + let part1 = 0; + let part2 = 1; + let value = super::connect_i32_into_u64(part1, part2); + assert_eq!(value, 1); + } + #[test] + fn test_connect_i32_into_u64_big() { + let part1 = -1; + let part2 = -1; + let value = super::connect_i32_into_u64(part1, part2); + assert_eq!(value, u64::MAX); + } + #[test] + fn test_connect_split_zero() { + for start_value in [0, 1, 10, u32::MAX as u64, i32::MAX as u64, u64::MAX] { + let (part1, part2) = split_u64_into_i32s(start_value); + let end_value = super::connect_i32_into_u64(part1, part2); + assert_eq!(start_value, end_value); + } + } +} diff --git a/krokiet/src/connect_delete.rs b/krokiet/src/connect_delete.rs index e40d79d..fb478a7 100644 --- a/krokiet/src/connect_delete.rs +++ b/krokiet/src/connect_delete.rs @@ -4,7 +4,7 @@ use std::path::MAIN_SEPARATOR; use czkawka_core::common::remove_folder_if_contains_only_empty_folders; -use crate::common::{get_is_header_mode, get_name_idx, get_path_idx, get_tool_model, set_tool_model}; +use crate::common::{get_is_header_mode, get_str_name_idx, get_str_path_idx, get_tool_model, set_tool_model}; use crate::{Callabler, CurrentTab, GuiState, MainListModel, MainWindow}; pub fn connect_delete_button(app: &MainWindow) { @@ -44,8 +44,8 @@ fn handle_delete_items(items: &ModelRc, active_tab: CurrentTab) - // and at the end should be send signal to main thread to update model // TODO handle also situations where cannot delete file/folder fn remove_selected_items(items: Vec, active_tab: CurrentTab) { - let path_idx = get_path_idx(active_tab); - let name_idx = get_name_idx(active_tab); + let path_idx = get_str_path_idx(active_tab); + let name_idx = get_str_name_idx(active_tab); let items_to_remove = items .iter() .map(|item| { diff --git a/krokiet/src/connect_scan.rs b/krokiet/src/connect_scan.rs index 5865ee6..3c68a3d 100644 --- a/krokiet/src/connect_scan.rs +++ b/krokiet/src/connect_scan.rs @@ -16,6 +16,7 @@ use czkawka_core::empty_folder::{EmptyFolder, FolderEntry}; use czkawka_core::similar_images; use czkawka_core::similar_images::{ImagesEntry, SimilarImages}; +use crate::common::split_u64_into_i32s; use crate::settings::{collect_settings, SettingsCustom, ALLOWED_HASH_TYPE_VALUES, ALLOWED_RESIZE_ALGORITHM_VALUES}; use crate::{CurrentTab, GuiState, MainListModel, MainWindow, ProgressToSend}; @@ -50,7 +51,6 @@ pub fn connect_scan_button(app: &MainWindow, progress_sender: Sender, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) @@ -70,6 +70,7 @@ fn scan_similar_images(a: Weak, progress_sender: Sender (ModelR directory.into(), NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(), ]); - let data_model_int = VecModel::from_slice(&[fe.width as i32, fe.height as i32]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let size_split = split_u64_into_i32s(fe.size); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1, fe.width as i32, fe.height as i32]); (data_model_str, data_model_int) } @@ -187,7 +190,9 @@ fn prepare_data_model_empty_files(fe: &FileEntry) -> (ModelRc, Mod directory.into(), NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(), ]); - let data_model_int = VecModel::from_slice(&[]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let size_split = split_u64_into_i32s(fe.size); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1]); (data_model_str, data_model_int) } @@ -230,7 +235,8 @@ fn prepare_data_model_empty_folders(fe: &FolderEntry) -> (ModelRc, directory.into(), NaiveDateTime::from_timestamp_opt(fe.modified_date as i64, 0).unwrap().to_string().into(), ]); - let data_model_int = VecModel::from_slice(&[]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1]); (data_model_str, data_model_int) } @@ -241,7 +247,7 @@ fn insert_data_to_model(items: &Rc>, data_model_str: Mod header_row, selected_row: false, val_str: ModelRc::new(data_model_str), - val_int: ModelRc::new(data_model_int), // TODO fill + val_int: ModelRc::new(data_model_int), }; items.push(main); } diff --git a/krokiet/src/connect_select.rs b/krokiet/src/connect_select.rs index b2a6e74..384ec40 100644 --- a/krokiet/src/connect_select.rs +++ b/krokiet/src/connect_select.rs @@ -1,6 +1,6 @@ -use crate::common::{get_tool_model, set_tool_model}; -use crate::SelectModel; +use crate::common::{connect_i32_into_u64, get_int_size_idx, get_is_header_mode, get_tool_model, set_tool_model}; use crate::{Callabler, GuiState, MainListModel, MainWindow, SelectMode}; +use crate::{CurrentTab, SelectModel}; use slint::{ComponentHandle, Model, ModelRc, VecModel}; // TODO optimize this, not sure if it is possible to not copy entire model to just select item @@ -16,6 +16,9 @@ pub fn connect_select(app: &MainWindow) { SelectMode::SelectAll => select_all(current_model), SelectMode::UnselectAll => deselect_all(current_model), SelectMode::InvertSelection => invert_selection(current_model), + SelectMode::SelectTheBiggestSize => select_the_biggest_size(current_model, active_tab), + SelectMode::SelectTheSmallestSize => select_the_small_size(current_model, active_tab), + _ => unimplemented!(), }; set_tool_model(&app, active_tab, new_model); }); @@ -31,10 +34,23 @@ pub fn connect_showing_proper_select_buttons(app: &MainWindow) { } fn set_select_buttons(app: &MainWindow) { - // let active_tab = app.global::().get_active_tab(); - let base_buttons = vec![SelectMode::SelectAll, SelectMode::UnselectAll, SelectMode::InvertSelection]; + let active_tab = app.global::().get_active_tab(); + let mut base_buttons = vec![SelectMode::SelectAll, SelectMode::UnselectAll, SelectMode::InvertSelection]; - // TODO Here needs to be put logic to set custom buttons depending on tab + let additional_buttons = match active_tab { + CurrentTab::SimilarImages => vec![ + SelectMode::SelectOldest, + SelectMode::SelectNewest, + SelectMode::SelectTheSmallestSize, + SelectMode::SelectTheBiggestSize, + SelectMode::SelectTheSmallestResolution, + SelectMode::SelectTheBiggestResolution, + ], + _ => vec![], + }; + + base_buttons.extend(additional_buttons); + base_buttons.reverse(); let new_select_model = base_buttons .into_iter() @@ -52,9 +68,65 @@ fn translate_select_mode(select_mode: SelectMode) -> String { SelectMode::SelectAll => "Select all".into(), SelectMode::UnselectAll => "Unselect all".into(), SelectMode::InvertSelection => "Invert selection".into(), + SelectMode::SelectTheBiggestSize => "Select the biggest size".into(), + SelectMode::SelectTheBiggestResolution => "Select the biggest resolution".into(), + SelectMode::SelectTheSmallestSize => "Select the smallest size".into(), + SelectMode::SelectTheSmallestResolution => "Select the smallest resolution".into(), + SelectMode::SelectNewest => "Select newest".into(), + SelectMode::SelectOldest => "Select oldest".into(), } } +fn select_the_biggest_size(model: ModelRc, active_tab: CurrentTab) -> ModelRc { + let is_header_mode = get_is_header_mode(active_tab); + assert!(is_header_mode); // non header modes not really have reasont to use this function + + let mut old_data = model.iter().collect::>(); + let headers_idx = find_header_idx_and_deselect_all(&mut old_data); + let size_idx = get_int_size_idx(active_tab); + + for i in 0..(headers_idx.len() - 1) { + let mut max_size = 0; + let mut max_size_idx = 0; + for j in (headers_idx[i] + 1)..headers_idx[i + 1] { + let int_data = old_data[j].val_int.iter().collect::>(); + let size = connect_i32_into_u64(int_data[size_idx], int_data[size_idx + 1]); + if size > max_size { + max_size = size; + max_size_idx = j; + } + } + old_data[max_size_idx].checked = true; + } + + ModelRc::new(VecModel::from(old_data)) +} + +fn select_the_small_size(model: ModelRc, active_tab: CurrentTab) -> ModelRc { + let is_header_mode = get_is_header_mode(active_tab); + assert!(is_header_mode); // non header modes not really have reasont to use this function + + let mut old_data = model.iter().collect::>(); + let headers_idx = find_header_idx_and_deselect_all(&mut old_data); + let size_idx = get_int_size_idx(active_tab); + + for i in 0..(headers_idx.len() - 1) { + let mut min_size = u64::MAX; + let mut min_size_idx = 0; + for j in (headers_idx[i] + 1)..headers_idx[i + 1] { + let int_data = old_data[j].val_int.iter().collect::>(); + let size = connect_i32_into_u64(int_data[size_idx], int_data[size_idx + 1]); + if size < min_size { + min_size = size; + min_size_idx = j; + } + } + old_data[min_size_idx].checked = true; + } + + ModelRc::new(VecModel::from(old_data)) +} + fn select_all(model: ModelRc) -> ModelRc { let mut old_data = model.iter().collect::>(); old_data.iter_mut().for_each(|x| { @@ -80,3 +152,64 @@ fn invert_selection(model: ModelRc) -> ModelRc { }); ModelRc::new(VecModel::from(old_data)) } + +fn find_header_idx_and_deselect_all(old_data: &mut Vec) -> Vec { + let mut header_idx = old_data + .iter() + .enumerate() + .filter_map(|(idx, m)| if m.header_row { Some(idx) } else { None }) + .collect::>(); + header_idx.push(old_data.len()); + + old_data.iter_mut().for_each(|x| { + if !x.header_row { + x.checked = false; + } + }); + header_idx +} + +#[cfg(test)] +mod test { + use crate::{MainListModel, SelectMode}; + use slint::ModelRc; + + // #[test] + // pub fn test_select_all() { + // let model = ModelRc::new(VecModel::from(vec![SelectModel { + // name: "test".into(), + // data: SelectMode::SelectAll, + // }])); + // let new_model = select_all(model); + // let new_data = new_model.iter().collect::>(); + // assert_eq!(new_data[0].checked, true); + // } + // + // fn prepare_simple_model() -> ModelRc { + // ModelRc::new(VecModel::from(vec![ + // MainListModel { + // header_row: false, + // checked: false, + // selected_row: false, + // val_str: [], + // val_int: [0, 0, 0, 0, 0, 0], + // }, + // MainListModel { + // header_row: false, + // checked: true, + // text: "test".into(), + // size: 0, + // resolution: (0, 0), + // date: 0, + // }, + // MainListModel { + // header_row: false, + // checked: false, + // text: "test".into(), + // size: 0, + // resolution: (0, 0), + // date: 0, + // }, + // ])) + // } +} diff --git a/krokiet/ui/action_buttons.slint b/krokiet/ui/action_buttons.slint index 5034ae3..e052fb8 100644 --- a/krokiet/ui/action_buttons.slint +++ b/krokiet/ui/action_buttons.slint @@ -72,7 +72,7 @@ export component ActionButtons inherits HorizontalLayout { enabled: !scanning && lists_enabled; text: "Select"; clicked => { - show_select_popup(self.x - self.width / 2, self.y + parent.y); + show_select_popup(self.x + self.width / 2, self.y + parent.y); } } diff --git a/krokiet/ui/common.slint b/krokiet/ui/common.slint index e5b276a..97b41b4 100644 --- a/krokiet/ui/common.slint +++ b/krokiet/ui/common.slint @@ -44,7 +44,13 @@ export struct ExcludedDirectoriesModel { export enum SelectMode { SelectAll, UnselectAll, - InvertSelection + InvertSelection, + SelectTheBiggestSize, + SelectTheBiggestResolution, + SelectTheSmallestSize, + SelectTheSmallestResolution, + SelectNewest, + SelectOldest, } export struct SelectModel { diff --git a/krokiet/ui/gui_state.slint b/krokiet/ui/gui_state.slint index 400ade8..84d38eb 100644 --- a/krokiet/ui/gui_state.slint +++ b/krokiet/ui/gui_state.slint @@ -19,5 +19,5 @@ export global GuiState { in-out property available_subsettings: active_tab == CurrentTab.SimilarImages; in-out property active_tab: CurrentTab.EmptyFiles; - in-out property <[SelectModel]> select_results_list: [{data: SelectMode.SelectAll, name: "Select All"}, {data: SelectMode.UnselectAll, name: "Deselect All"}]; + in-out property <[SelectModel]> select_results_list: [{data: SelectMode.SelectAll, name: "Select All"}, {data: SelectMode.UnselectAll, name: "Deselect All"}, {data: SelectMode.SelectTheSmallestResolution, name: "Select the smallest resolution"}]; } diff --git a/krokiet/ui/main_window.slint b/krokiet/ui/main_window.slint index ad81e75..f3f3de3 100644 --- a/krokiet/ui/main_window.slint +++ b/krokiet/ui/main_window.slint @@ -119,7 +119,7 @@ export component MainWindow inherits Window { } show_select_popup(x_offset, y_offset) => { select_popup_window.x_offset = x_offset; - select_popup_window.y_offset = y_offset - select-popup-window.all_items_height - 5px; + select_popup_window.y_offset = y_offset; select_popup_window.show_popup(); } } @@ -151,8 +151,8 @@ export component MainWindow inherits Window { property x_offset: 0; property y_offset: 0; - x: parent.x + x_offset; - y: parent.y + y_offset; + x: parent.x + x_offset - self.item_width / 2.0; + y: parent.y + y_offset - self.all_items_height - 5px; height: root.height; width: root.width; diff --git a/krokiet/ui/popup_select_results.slint b/krokiet/ui/popup_select_results.slint index 0d02844..e99790e 100644 --- a/krokiet/ui/popup_select_results.slint +++ b/krokiet/ui/popup_select_results.slint @@ -17,7 +17,7 @@ export component PopupSelectResults inherits Rectangle { callback show_popup(); property <[SelectModel]> model: GuiState.select_results_list; property item_height: 30px; - property item_width: 140px; + out property item_width: 200px; out property all_items_height: item_height * model.length; popup_window := PopupWindow { diff --git a/krokiet/ui/tool_settings.slint b/krokiet/ui/tool_settings.slint index c017050..84b28ef 100644 --- a/krokiet/ui/tool_settings.slint +++ b/krokiet/ui/tool_settings.slint @@ -69,21 +69,21 @@ export component ToolSettings { ComboBoxWrapper { text: "Hash size"; model: Settings.similar_images_sub_available_hash_size; - current_index: Settings.similar_images_sub_hash_size_index; + current_index <=> Settings.similar_images_sub_hash_size_index; } ComboBoxWrapper { text: "Resize Algorithm"; model: Settings.similar_images_sub_available_resize_algorithm; - current_index: Settings.similar_images_sub_resize_algorithm_index; + current_index <=> Settings.similar_images_sub_resize_algorithm_index; } ComboBoxWrapper { text: "Hash type"; model: Settings.similar_images_sub_available_hash_type; - current_index: Settings.similar_images_sub_hash_type_index; + current_index <=> Settings.similar_images_sub_hash_type_index; } CheckBoxWrapper { text: "Ignore same size"; - checked: Settings.similar_images_sub_ignore_same_size; + checked <=> Settings.similar_images_sub_ignore_same_size; } SliderWrapper { text: "Max difference";