diff --git a/krokiet/src/connect_delete.rs b/krokiet/src/connect_delete.rs index fb478a7..b0c1df9 100644 --- a/krokiet/src/connect_delete.rs +++ b/krokiet/src/connect_delete.rs @@ -1,10 +1,10 @@ use rayon::prelude::*; -use slint::{ComponentHandle, Model, ModelRc, VecModel}; -use std::path::MAIN_SEPARATOR; +use slint::{ComponentHandle, ModelRc, VecModel}; use czkawka_core::common::remove_folder_if_contains_only_empty_folders; -use crate::common::{get_is_header_mode, get_str_name_idx, get_str_path_idx, get_tool_model, set_tool_model}; +use crate::common::{get_is_header_mode, get_tool_model, set_tool_model}; +use crate::model_operations::{collect_full_path_from_model, deselect_all_items, filter_out_checked_items}; use crate::{Callabler, CurrentTab, GuiState, MainListModel, MainWindow}; pub fn connect_delete_button(app: &MainWindow) { @@ -30,7 +30,8 @@ fn handle_delete_items(items: &ModelRc, active_tab: CurrentTab) - let (entries_to_delete, mut entries_left) = filter_out_checked_items(items, get_is_header_mode(active_tab)); if !entries_to_delete.is_empty() { - remove_selected_items(entries_to_delete, active_tab); + let vec_items_to_remove = collect_full_path_from_model(&entries_to_delete, active_tab); + remove_selected_items(vec_items_to_remove, active_tab); deselect_all_items(&mut entries_left); let r = ModelRc::new(VecModel::from(entries_left)); // TODO here maybe should also stay old model if entries cannot be removed @@ -43,18 +44,7 @@ fn handle_delete_items(items: &ModelRc, active_tab: CurrentTab) - // For empty folders double check if folders are really empty - this function probably should be run in thread // 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_str_path_idx(active_tab); - let name_idx = get_str_name_idx(active_tab); - let items_to_remove = items - .iter() - .map(|item| { - let path = item.val_str.iter().nth(path_idx).unwrap(); - let name = item.val_str.iter().nth(name_idx).unwrap(); - format!("{}{}{}", path, MAIN_SEPARATOR, name) - }) - .collect::>(); - +fn remove_selected_items(items_to_remove: Vec, active_tab: CurrentTab) { // Iterate over empty folders and not delete them if they are not empty if active_tab == CurrentTab::EmptyFolders { items_to_remove.into_par_iter().for_each(|item| { @@ -66,202 +56,3 @@ fn remove_selected_items(items: Vec, active_tab: CurrentTab) { }); } } - -fn deselect_all_items(items: &mut [MainListModel]) { - for item in items { - item.selected_row = false; - } -} - -fn filter_out_checked_items(items: &ModelRc, have_header: bool) -> (Vec, Vec) { - if cfg!(debug_assertions) { - check_if_header_is_checked(items); - check_if_header_is_selected_but_should_not_be(items, have_header); - } - - let (entries_to_delete, mut entries_left): (Vec<_>, Vec<_>) = items.iter().partition(|item| item.checked); - - // When have header, we must also throw out orphaned items - this needs to be - if have_header && !entries_left.is_empty() { - // First row must be header - assert!(entries_left[0].header_row); - - if entries_left.len() == 3 { - // First row is header, so if second or third is also header, then there is no enough items to fill model - if entries_left[1].header_row || entries_left[2].header_row { - entries_left = Vec::new(); - } - } else if entries_left.len() < 3 { - // Not have enough items to fill model - entries_left = Vec::new(); - } else { - let mut last_header = 0; - let mut new_items: Vec = Vec::new(); - for i in 1..entries_left.len() { - if entries_left[i].header_row { - if i - last_header > 2 { - new_items.extend(entries_left[last_header..i].iter().cloned()); - } - last_header = i; - } - } - if entries_left.len() - last_header > 2 { - new_items.extend(entries_left[last_header..].iter().cloned()); - } - - entries_left = new_items; - } - } - - (entries_to_delete, entries_left) -} - -// Function to verify if really headers are not checked -// Checked header is big bug -fn check_if_header_is_checked(items: &ModelRc) { - if cfg!(debug_assertions) { - for item in items.iter() { - if item.header_row { - assert!(!item.checked); - } - } - } -} - -// In some modes header should not be visible, but if are, then it is a bug -fn check_if_header_is_selected_but_should_not_be(items: &ModelRc, can_have_header: bool) { - if cfg!(debug_assertions) { - if !can_have_header { - for item in items.iter() { - assert!(!item.header_row); - } - } - } -} - -#[cfg(test)] -mod tests { - use slint::{Model, ModelRc, SharedString, VecModel}; - - use crate::connect_delete::filter_out_checked_items; - use crate::MainListModel; - - #[test] - fn test_filter_out_checked_items_empty() { - let items: ModelRc = create_new_model(vec![]); - - let (to_delete, left) = filter_out_checked_items(&items, false); - assert!(to_delete.is_empty()); - assert!(left.is_empty()); - let (to_delete, left) = filter_out_checked_items(&items, true); - assert!(to_delete.is_empty()); - assert!(left.is_empty()); - } - #[test] - fn test_filter_out_checked_items_one_element_valid_normal() { - let items = create_new_model(vec![(false, false, false, vec![])]); - let (to_delete, left) = filter_out_checked_items(&items, false); - assert!(to_delete.is_empty()); - assert_eq!(left.len(), items.iter().count()); - } - - #[test] - fn test_filter_out_checked_items_one_element_valid_header() { - let items = create_new_model(vec![(false, true, false, vec![])]); - let (to_delete, left) = filter_out_checked_items(&items, true); - assert!(to_delete.is_empty()); - assert!(left.is_empty()); - } - - #[test] - #[should_panic] - fn test_filter_out_checked_items_one_element_invalid_normal() { - let items = create_new_model(vec![(false, true, false, vec![])]); - filter_out_checked_items(&items, false); - } - #[test] - #[should_panic] - fn test_filter_out_checked_items_one_element_invalid_header() { - let items = create_new_model(vec![(false, false, false, vec![])]); - filter_out_checked_items(&items, true); - } - - #[test] - fn test_filter_out_checked_items_multiple_element_valid_normal() { - let items = create_new_model(vec![ - (false, false, false, vec!["1"]), - (false, false, false, vec!["2"]), - (true, false, false, vec!["3"]), - (true, false, false, vec!["4"]), - (false, false, false, vec!["5"]), - ]); - let (to_delete, left) = filter_out_checked_items(&items, false); - let to_delete_data = get_single_data_str_from_model(&to_delete); - let left_data = get_single_data_str_from_model(&left); - - assert_eq!(to_delete_data, vec!["3", "4"]); - assert_eq!(left_data, vec!["1", "2", "5"]); - } - - #[test] - fn test_filter_out_checked_items_multiple_element_valid_header() { - let items = create_new_model(vec![ - (false, true, false, vec!["1"]), - (false, false, false, vec!["2"]), - (true, false, false, vec!["3"]), - (false, true, false, vec!["4"]), - (false, false, false, vec!["5"]), - (false, true, false, vec!["6"]), - (false, false, false, vec!["7"]), - (false, false, false, vec!["8"]), - ]); - let (to_delete, left) = filter_out_checked_items(&items, true); - let to_delete_data = get_single_data_str_from_model(&to_delete); - let left_data = get_single_data_str_from_model(&left); - - assert_eq!(to_delete_data, vec!["3"]); - assert_eq!(left_data, vec!["6", "7", "8"]); - } - - #[test] - fn test_filter_out_checked_items_multiple2_element_valid_header() { - let items = create_new_model(vec![ - (false, true, false, vec!["1"]), - (false, false, false, vec!["2"]), - (true, false, false, vec!["3"]), - (false, false, false, vec!["4"]), - (false, false, false, vec!["5"]), - (false, false, false, vec!["6"]), - (false, true, false, vec!["7"]), - (false, false, false, vec!["8"]), - ]); - let (to_delete, left) = filter_out_checked_items(&items, true); - let to_delete_data = get_single_data_str_from_model(&to_delete); - let left_data = get_single_data_str_from_model(&left); - - assert_eq!(to_delete_data, vec!["3"]); - assert_eq!(left_data, vec!["1", "2", "4", "5", "6"]); - } - - fn get_single_data_str_from_model(model: &[MainListModel]) -> Vec { - let mut d = model.iter().map(|item| item.val_str.iter().next().unwrap().to_string()).collect::>(); - d.sort(); - d - } - - fn create_new_model(items: Vec<(bool, bool, bool, Vec<&'static str>)>) -> ModelRc { - let model = VecModel::default(); - for item in items { - let all_items: Vec = item.3.iter().map(|item| (*item).into()).collect::>(); - let all_items = VecModel::from(all_items); - model.push(MainListModel { - checked: item.0, - header_row: item.1, - selected_row: item.2, - val_str: ModelRc::new(all_items), - val_int: ModelRc::new(VecModel::default()), - }); - } - ModelRc::new(model) - } -} diff --git a/krokiet/src/connect_move.rs b/krokiet/src/connect_move.rs new file mode 100644 index 0000000..6f07484 --- /dev/null +++ b/krokiet/src/connect_move.rs @@ -0,0 +1,106 @@ +use crate::common::{get_is_header_mode, get_tool_model}; +use crate::model_operations::{collect_path_name_from_model, deselect_all_items, filter_out_checked_items}; +use crate::CurrentTab; +use crate::{Callabler, GuiState, MainListModel, MainWindow}; + +use rayon::prelude::*; +use slint::{ComponentHandle, ModelRc, VecModel}; +use std::path::{Path, PathBuf}; +use std::{fs, path}; + +pub fn connect_move(app: &MainWindow) { + let a = app.as_weak(); + // app.global::().on_move_items(move |select_mode| { + // let app = a.upgrade().unwrap(); + // let active_tab = app.global::().get_active_tab(); + // let current_model = get_tool_model(&app, active_tab); + // + // // If tree structure will be + // let preserve_structure = false; + // let copy_mode = true; + // let output_folder = "/home/rafal/Downloads/AAAAAAAA"; + // move_operation(¤t_model, preserve_structure, copy_mode, output_folder, active_tab); + // }); + // move_selected_items(vec![("/home/rafal/".to_string(), "Other.png".to_string())], true, true, "/home/rafal/Downloads/AAAAAAAA"); +} + +fn move_operation(items: &ModelRc, preserve_structure: bool, copy_mode: bool, output_folder: &str, active_tab: CurrentTab) -> Option> { + let (entries_to_move, mut entries_left) = filter_out_checked_items(items, get_is_header_mode(active_tab)); + + if !entries_to_move.is_empty() { + let vec_items_to_move = collect_path_name_from_model(&entries_to_move, active_tab); + move_selected_items(vec_items_to_move, preserve_structure, copy_mode, output_folder); + deselect_all_items(&mut entries_left); + + let r = ModelRc::new(VecModel::from(entries_left)); + return Some(r); + } + None +} + +fn move_selected_items(items_to_move: Vec<(String, String)>, preserve_structure: bool, copy_mode: bool, output_folder: &str) -> Vec { + if let Err(err) = fs::create_dir_all(output_folder) { + return vec![format!("Error while creating folder: {err}")]; + } + + if copy_mode { + items_to_move + .into_par_iter() + .filter_map(|(path, name)| { + let (input_file, output_file) = collect_path_and_create_folders(&path, &name, output_folder, preserve_structure); + + if let Err(e) = fs::copy(&input_file, &output_file) { + return Some(format!("Error while copying file {input_file:?} to {output_file:?}, reason {e}")); + } + None + }) + .collect() + } else { + items_to_move + .into_par_iter() + .filter_map(|(path, name)| { + let (input_file, output_file) = collect_path_and_create_folders(&path, &name, output_folder, preserve_structure); + + if output_file.exists() { + return Some(format!("File {output_file:?} already exists")); + } + + // Try to rename file, may fail due various reasons + if fs::rename(&input_file, &output_file).is_ok() { + return None; + } + + // It is possible that this failed, because file is on different partition, so + // we need to copy file and then remove old + if let Err(e) = fs::copy(&input_file, &output_file) { + return Some(format!( + "Error while copying file {input_file:?} to {output_file:?}(moving into different partition), reason {e}" + )); + } + if let Err(e) = fs::remove_file(&input_file) { + return Some(format!("Error while removing file {input_file:?}, reason {e}")); + } + + None + }) + .collect() + } +} + +// Create input/output paths, and create output folder +fn collect_path_and_create_folders(input_path: &str, input_file: &str, output_path: &str, preserve_structure: bool) -> (PathBuf, PathBuf) { + let mut input_full_path = PathBuf::from(input_path); + input_full_path.push(input_file); + + let mut output_full_path = PathBuf::from(output_path); + if preserve_structure { + output_full_path.extend(Path::new(input_path).components().filter(|c| match c { + path::Component::Normal(_) => true, + _ => false, + })); + }; + let _ = fs::create_dir_all(&output_full_path); + output_full_path.push(input_file); + + (input_full_path, output_full_path) +} diff --git a/krokiet/src/main.rs b/krokiet/src/main.rs index f8ca60a..b6fd86f 100644 --- a/krokiet/src/main.rs +++ b/krokiet/src/main.rs @@ -27,6 +27,7 @@ use czkawka_core::common_dir_traversal::ProgressData; use crate::connect_delete::connect_delete_button; use crate::connect_directories_changes::connect_add_remove_directories; +use crate::connect_move::connect_move; use crate::connect_open::connect_open_items; use crate::connect_progress_receiver::connect_progress_gathering; use crate::connect_scan::connect_scan_button; @@ -40,6 +41,7 @@ use crate::settings::{connect_changing_settings_preset, create_default_settings_ mod common; mod connect_delete; mod connect_directories_changes; +mod connect_move; mod connect_open; mod connect_progress_receiver; mod connect_scan; @@ -48,13 +50,10 @@ mod connect_show_preview; mod connect_stop; mod connect_translation; mod localizer_krokiet; +mod model_operations; mod set_initial_gui_info; mod settings; -// use std::rc::Rc; - -// use slint::{ModelRc, VecModel}; - slint::include_modules!(); fn main() { setup_logger(false); @@ -87,6 +86,7 @@ fn main() { connect_changing_settings_preset(&app); connect_select(&app); connect_showing_proper_select_buttons(&app); + connect_move(&app); app.run().unwrap(); diff --git a/krokiet/src/model_operations.rs b/krokiet/src/model_operations.rs new file mode 100644 index 0000000..c1ff591 --- /dev/null +++ b/krokiet/src/model_operations.rs @@ -0,0 +1,236 @@ +use crate::common::{get_str_name_idx, get_str_path_idx}; +use crate::{CurrentTab, MainListModel}; +use slint::{Model, ModelRc}; +use std::path::MAIN_SEPARATOR; + +pub fn deselect_all_items(items: &mut [MainListModel]) { + for item in items { + item.selected_row = false; + } +} +pub fn select_all_items(items: &mut [MainListModel]) { + for item in items { + item.selected_row = true; + } +} + +pub fn collect_full_path_from_model(items: &[MainListModel], active_tab: CurrentTab) -> Vec { + let path_idx = get_str_path_idx(active_tab); + let name_idx = get_str_name_idx(active_tab); + items + .iter() + .map(|item| { + let path = item.val_str.iter().nth(path_idx).unwrap(); + let name = item.val_str.iter().nth(name_idx).unwrap(); + format!("{}{}{}", path, MAIN_SEPARATOR, name) + }) + .collect::>() +} +pub fn collect_path_name_from_model(items: &[MainListModel], active_tab: CurrentTab) -> Vec<(String, String)> { + let path_idx = get_str_path_idx(active_tab); + let name_idx = get_str_name_idx(active_tab); + items + .iter() + .map(|item| { + dbg!(item.val_str.iter().nth(path_idx).unwrap().to_string()); + dbg!(item.val_str.iter().nth(name_idx).unwrap().to_string()); + ( + item.val_str.iter().nth(path_idx).unwrap().to_string(), + item.val_str.iter().nth(name_idx).unwrap().to_string(), + ) + }) + .collect::>() +} + +pub fn filter_out_checked_items(items: &ModelRc, have_header: bool) -> (Vec, Vec) { + if cfg!(debug_assertions) { + check_if_header_is_checked(items); + check_if_header_is_selected_but_should_not_be(items, have_header); + } + + let (entries_to_delete, mut entries_left): (Vec<_>, Vec<_>) = items.iter().partition(|item| item.checked); + + // When have header, we must also throw out orphaned items - this needs to be + if have_header && !entries_left.is_empty() { + // First row must be header + assert!(entries_left[0].header_row); + + if entries_left.len() == 3 { + // First row is header, so if second or third is also header, then there is no enough items to fill model + if entries_left[1].header_row || entries_left[2].header_row { + entries_left = Vec::new(); + } + } else if entries_left.len() < 3 { + // Not have enough items to fill model + entries_left = Vec::new(); + } else { + let mut last_header = 0; + let mut new_items: Vec = Vec::new(); + for i in 1..entries_left.len() { + if entries_left[i].header_row { + if i - last_header > 2 { + new_items.extend(entries_left[last_header..i].iter().cloned()); + } + last_header = i; + } + } + if entries_left.len() - last_header > 2 { + new_items.extend(entries_left[last_header..].iter().cloned()); + } + + entries_left = new_items; + } + } + + (entries_to_delete, entries_left) +} + +// Function to verify if really headers are not checked +// Checked header is big bug +fn check_if_header_is_checked(items: &ModelRc) { + if cfg!(debug_assertions) { + for item in items.iter() { + if item.header_row { + assert!(!item.checked); + } + } + } +} + +// In some modes header should not be visible, but if are, then it is a bug +fn check_if_header_is_selected_but_should_not_be(items: &ModelRc, can_have_header: bool) { + if cfg!(debug_assertions) { + if !can_have_header { + for item in items.iter() { + assert!(!item.header_row); + } + } + } +} + +#[cfg(test)] +mod tests { + use slint::{Model, ModelRc, SharedString, VecModel}; + + use crate::model_operations::filter_out_checked_items; + use crate::MainListModel; + + #[test] + fn test_filter_out_checked_items_empty() { + let items: ModelRc = create_new_model(vec![]); + + let (to_delete, left) = filter_out_checked_items(&items, false); + assert!(to_delete.is_empty()); + assert!(left.is_empty()); + let (to_delete, left) = filter_out_checked_items(&items, true); + assert!(to_delete.is_empty()); + assert!(left.is_empty()); + } + #[test] + fn test_filter_out_checked_items_one_element_valid_normal() { + let items = create_new_model(vec![(false, false, false, vec![])]); + let (to_delete, left) = filter_out_checked_items(&items, false); + assert!(to_delete.is_empty()); + assert_eq!(left.len(), items.iter().count()); + } + + #[test] + fn test_filter_out_checked_items_one_element_valid_header() { + let items = create_new_model(vec![(false, true, false, vec![])]); + let (to_delete, left) = filter_out_checked_items(&items, true); + assert!(to_delete.is_empty()); + assert!(left.is_empty()); + } + + #[test] + #[should_panic] + fn test_filter_out_checked_items_one_element_invalid_normal() { + let items = create_new_model(vec![(false, true, false, vec![])]); + filter_out_checked_items(&items, false); + } + #[test] + #[should_panic] + fn test_filter_out_checked_items_one_element_invalid_header() { + let items = create_new_model(vec![(false, false, false, vec![])]); + filter_out_checked_items(&items, true); + } + + #[test] + fn test_filter_out_checked_items_multiple_element_valid_normal() { + let items = create_new_model(vec![ + (false, false, false, vec!["1"]), + (false, false, false, vec!["2"]), + (true, false, false, vec!["3"]), + (true, false, false, vec!["4"]), + (false, false, false, vec!["5"]), + ]); + let (to_delete, left) = filter_out_checked_items(&items, false); + let to_delete_data = get_single_data_str_from_model(&to_delete); + let left_data = get_single_data_str_from_model(&left); + + assert_eq!(to_delete_data, vec!["3", "4"]); + assert_eq!(left_data, vec!["1", "2", "5"]); + } + + #[test] + fn test_filter_out_checked_items_multiple_element_valid_header() { + let items = create_new_model(vec![ + (false, true, false, vec!["1"]), + (false, false, false, vec!["2"]), + (true, false, false, vec!["3"]), + (false, true, false, vec!["4"]), + (false, false, false, vec!["5"]), + (false, true, false, vec!["6"]), + (false, false, false, vec!["7"]), + (false, false, false, vec!["8"]), + ]); + let (to_delete, left) = filter_out_checked_items(&items, true); + let to_delete_data = get_single_data_str_from_model(&to_delete); + let left_data = get_single_data_str_from_model(&left); + + assert_eq!(to_delete_data, vec!["3"]); + assert_eq!(left_data, vec!["6", "7", "8"]); + } + + #[test] + fn test_filter_out_checked_items_multiple2_element_valid_header() { + let items = create_new_model(vec![ + (false, true, false, vec!["1"]), + (false, false, false, vec!["2"]), + (true, false, false, vec!["3"]), + (false, false, false, vec!["4"]), + (false, false, false, vec!["5"]), + (false, false, false, vec!["6"]), + (false, true, false, vec!["7"]), + (false, false, false, vec!["8"]), + ]); + let (to_delete, left) = filter_out_checked_items(&items, true); + let to_delete_data = get_single_data_str_from_model(&to_delete); + let left_data = get_single_data_str_from_model(&left); + + assert_eq!(to_delete_data, vec!["3"]); + assert_eq!(left_data, vec!["1", "2", "4", "5", "6"]); + } + + fn get_single_data_str_from_model(model: &[MainListModel]) -> Vec { + let mut d = model.iter().map(|item| item.val_str.iter().next().unwrap().to_string()).collect::>(); + d.sort(); + d + } + + fn create_new_model(items: Vec<(bool, bool, bool, Vec<&'static str>)>) -> ModelRc { + let model = VecModel::default(); + for item in items { + let all_items: Vec = item.3.iter().map(|item| (*item).into()).collect::>(); + let all_items = VecModel::from(all_items); + model.push(MainListModel { + checked: item.0, + header_row: item.1, + selected_row: item.2, + val_str: ModelRc::new(all_items), + val_int: ModelRc::new(VecModel::default()), + }); + } + ModelRc::new(model) + } +} diff --git a/krokiet/ui/action_buttons.slint b/krokiet/ui/action_buttons.slint index b13dcfa..b11d710 100644 --- a/krokiet/ui/action_buttons.slint +++ b/krokiet/ui/action_buttons.slint @@ -58,6 +58,15 @@ export component ActionButtons inherits HorizontalLayout { horizontal-stretch: 0.5; } + move_button := Button { + height: parent.height; + enabled: !scanning && lists_enabled; + text: "Move"; + clicked => { + // show_move_popup(self.x + self.width / 2, self.y + parent.y); + } + } + select_button := Button { height: parent.height; enabled: !scanning && lists_enabled; diff --git a/krokiet/ui/popup_select_results.slint b/krokiet/ui/popup_select_results.slint index e99790e..35081b6 100644 --- a/krokiet/ui/popup_select_results.slint +++ b/krokiet/ui/popup_select_results.slint @@ -21,7 +21,6 @@ export component PopupSelectResults inherits Rectangle { out property all_items_height: item_height * model.length; popup_window := PopupWindow { - width: item_width; height: all_items_height;