From 1178e145aeb4a8505d2208e7d891c1456c81ea96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Mikrut?= <41945903+qarmin@users.noreply.github.com> Date: Thu, 15 Oct 2020 09:04:02 +0200 Subject: [PATCH] Add similar images support for GUI (#69) --- Changelog.md | 7 +- Instruction.md | 2 +- README.md | 1 + czkawka_core/src/similar_files.rs | 7 + czkawka_gui/czkawka.glade | 78 +++++++++++ czkawka_gui/src/help_functions.rs | 68 +++++++++- czkawka_gui/src/main.rs | 208 +++++++++++++++++++++++++++--- 7 files changed, 347 insertions(+), 24 deletions(-) diff --git a/Changelog.md b/Changelog.md index 58ba89c..5831202 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ ## Version 1.1.1 - -- Add test suite to PR +- Replace String with PathBuf for paths [#59](https://github.com/qarmin/czkawka/pull/59) +- Add test suite to PR [#65](https://github.com/qarmin/czkawka/pull/65) +- Support for finding similar images to CLI [#66](https://github.com/qarmin/czkawka/pull/66) +- Fix grammar-related errors and Ponglish expressions [#62](https://github.com/qarmin/czkawka/pull/62), [#63](https://github.com/qarmin/czkawka/pull/63) +- Don't delete by default files in duplicate finder in CLI - [23f203](https://github.com/qarmin/czkawka/commit/23f203a061e254275c95ca23ca4f1a78bd941f02) @@ -19,6 +23,7 @@ - Fixed crash with invalid file modification date [#33](https://github.com/qarmin/czkawka/issues/33) - Upper tabs can hide and show when this is necessary [#38](https://github.com/qarmin/czkawka/pull/38) - Fixed crash when file/folder name have non Unicode character [#44](https://github.com/qarmin/czkawka/issues/44) +- Added support for finding similar pictures in GUI [#69](https://github.com/qarmin/czkawka/issues/69) ## Version 1.0.0 - 02.10.2020r - Added confirmation button to delete button diff --git a/Instruction.md b/Instruction.md index 8034722..500235e 100644 --- a/Instruction.md +++ b/Instruction.md @@ -1,5 +1,5 @@ # Instruction - +TODO ## GUI diff --git a/README.md b/README.md index f3bc04b..26dab8d 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ Differences should be more visible when using slower CPU or faster disk. | Empty folders | X | X | | Temporary files | X | X | | Big files | X | | +| Similar images | X | | | Installed packages | | X | | Invalid names | | X | | Names conflict | | X | diff --git a/czkawka_core/src/similar_files.rs b/czkawka_core/src/similar_files.rs index 6e4557e..127277b 100644 --- a/czkawka_core/src/similar_files.rs +++ b/czkawka_core/src/similar_files.rs @@ -95,6 +95,10 @@ impl SimilarImages { &self.text_messages } + pub const fn get_similar_images(&self) -> &Vec { + &self.similar_vectors + } + pub const fn get_information(&self) -> &Info { &self.information } @@ -259,6 +263,9 @@ impl SimilarImages { let mut new_vector: Vec = Vec::new(); for (string_hash, vec_file_entry) in &self.image_hashes { + if rx.is_some() && rx.unwrap().try_recv().is_ok() { + return false; + } let vector_with_found_similar_hashes = self.bktree.find(string_hash.as_str(), 3).collect::>(); if vector_with_found_similar_hashes.len() == 1 && vec_file_entry.len() == 1 { // Exists only 1 unique picture, so there is no need to use it diff --git a/czkawka_gui/czkawka.glade b/czkawka_gui/czkawka.glade index 49d94e6..83d6c77 100644 --- a/czkawka_gui/czkawka.glade +++ b/czkawka_gui/czkawka.glade @@ -970,6 +970,84 @@ Author: RafaƂ Mikrut False + + + True + False + vertical + + + True + False + 8 + + + True + False + Minimal file size(in bytes) + + + False + True + 0 + + + + + True + True + 15 + 16384 + False + number + + + True + True + 1 + + + + + False + True + 0 + + + + + + + + True + True + in + + + + + + True + True + 2 + + + + + 5 + + + + + True + False + Similar Images + + + 5 + False + + True diff --git a/czkawka_gui/src/help_functions.rs b/czkawka_gui/src/help_functions.rs index a81b9b5..90734cc 100644 --- a/czkawka_gui/src/help_functions.rs +++ b/czkawka_gui/src/help_functions.rs @@ -1,4 +1,5 @@ use czkawka_core::common_messages::Messages; +use czkawka_core::similar_files::Similarity; use gtk::prelude::*; use gtk::TreeViewColumn; use std::collections::HashMap; @@ -39,6 +40,12 @@ pub enum ColumnsTemporaryFiles { Path, Modification, } +pub enum ColumnsSimilarImages { + Similarity = 0, + Name, + Path, + Modification, +} pub const TEXT_COLOR: &str = "#ffffff"; pub const MAIN_ROW_COLOR: &str = "#343434"; @@ -216,6 +223,46 @@ pub fn create_tree_view_empty_files(tree_view: &mut gtk::TreeView) { tree_view.set_vexpand(true); } +pub fn create_tree_view_similar_images(tree_view: &mut gtk::TreeView) { + let renderer = gtk::CellRendererText::new(); + let name_column: gtk::TreeViewColumn = TreeViewColumn::new(); + name_column.pack_start(&renderer, true); + name_column.set_title("Similarity"); + name_column.set_resizable(true); + name_column.set_min_width(50); + name_column.add_attribute(&renderer, "text", ColumnsSimilarImages::Similarity as i32); + + tree_view.append_column(&name_column); + let renderer = gtk::CellRendererText::new(); + let name_column: gtk::TreeViewColumn = TreeViewColumn::new(); + name_column.pack_start(&renderer, true); + name_column.set_title("File Name"); + name_column.set_resizable(true); + name_column.set_min_width(50); + name_column.add_attribute(&renderer, "text", ColumnsSimilarImages::Name as i32); + tree_view.append_column(&name_column); + + let renderer = gtk::CellRendererText::new(); + let path_column: gtk::TreeViewColumn = TreeViewColumn::new(); + path_column.pack_start(&renderer, true); + path_column.set_title("Path"); + path_column.set_resizable(true); + path_column.set_min_width(100); + path_column.add_attribute(&renderer, "text", ColumnsSimilarImages::Path as i32); + tree_view.append_column(&path_column); + + let renderer = gtk::CellRendererText::new(); + let modification_date_column: gtk::TreeViewColumn = TreeViewColumn::new(); + modification_date_column.pack_start(&renderer, true); + modification_date_column.set_title("Modification Date"); + modification_date_column.set_resizable(true); + modification_date_column.set_min_width(100); + modification_date_column.add_attribute(&renderer, "text", ColumnsSimilarImages::Modification as i32); + tree_view.append_column(&modification_date_column); + + tree_view.set_vexpand(true); +} + pub fn create_tree_view_directories(tree_view: &mut gtk::TreeView) { let renderer = gtk::CellRendererText::new(); let name_column: gtk::TreeViewColumn = TreeViewColumn::new(); @@ -285,7 +332,7 @@ pub fn print_text_messages_to_text_view(text_messages: &Messages, text_view: > text_view.get_buffer().unwrap().set_text(messages.as_str()); } -pub fn select_function_3column(_tree_selection: >k::TreeSelection, tree_model: >k::TreeModel, tree_path: >k::TreePath, _is_path_currently_selected: bool) -> bool { +pub fn select_function_duplicates(_tree_selection: >k::TreeSelection, tree_model: >k::TreeModel, tree_path: >k::TreePath, _is_path_currently_selected: bool) -> bool { // let name = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(),ColumnsDuplicates::Name as i32).get::().unwrap().unwrap(); // let path = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(), ColumnsDuplicates::Path as i32).get::().unwrap().unwrap(); // let modification = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(),ColumnsDuplicates::Modification as i32).get::().unwrap().unwrap(); @@ -297,6 +344,15 @@ pub fn select_function_3column(_tree_selection: >k::TreeSelection, tree_model: true } +pub fn select_function_similar_images(_tree_selection: >k::TreeSelection, tree_model: >k::TreeModel, tree_path: >k::TreePath, _is_path_currently_selected: bool) -> bool { + let path = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(), ColumnsSimilarImages::Path as i32).get::().unwrap().unwrap(); + + if path.trim() == "" { + return false; + } + + true +} pub fn set_buttons(hashmap: &mut HashMap, buttons_array: &[gtk::Button], button_names: &[&str]) { for (index, button) in buttons_array.iter().enumerate() { @@ -322,3 +378,13 @@ pub fn hide_all_buttons_except(except_name: &str, buttons_array: &[gtk::Button], } } } + +pub fn get_text_from_similarity(similarity: &Similarity) -> &str { + match similarity { + Similarity::None => "Original", + Similarity::Small => "Small", + Similarity::Medium => "Medium", + Similarity::High => "High", + Similarity::VeryHigh => "Very High", + } +} diff --git a/czkawka_gui/src/main.rs b/czkawka_gui/src/main.rs index a756b88..785cdad 100644 --- a/czkawka_gui/src/main.rs +++ b/czkawka_gui/src/main.rs @@ -12,6 +12,7 @@ use czkawka_core::common_traits::SaveResults; use czkawka_core::duplicate::CheckingMethod; use czkawka_core::empty_files::EmptyFiles; use czkawka_core::empty_folder::EmptyFolder; +use czkawka_core::similar_files::SimilarImages; use czkawka_core::temporary::Temporary; use duplicate::DuplicateFinder; use gtk::prelude::*; @@ -61,7 +62,7 @@ fn main() { //////////////////////////////////////////////////////////////////////////////////////////////// //// States - let main_notebooks_labels = ["duplicate", "empty_folder", "empty_file", "temporary_file", "big_file"]; + let main_notebooks_labels = ["duplicate", "empty_folder", "empty_file", "temporary_file", "big_file", "similar_images"]; let upper_notebooks_labels = [/*"general",*/ "included_directories", "excluded_directories", "excluded_items", "allowed_extensions"]; let buttons_labels = ["search", "stop", "resume", "pause", "select", "delete", "save"]; @@ -104,6 +105,7 @@ fn main() { let shared_empty_files_state: Rc> = Rc::new(RefCell::new(EmptyFiles::new())); let shared_temporary_files_state: Rc> = Rc::new(RefCell::new(Temporary::new())); let shared_big_files_state: Rc> = Rc::new(RefCell::new(BigFile::new())); + let shared_similar_images_state: Rc> = Rc::new(RefCell::new(SimilarImages::new())); // State of confirmation dialogs let shared_confirmation_dialog_delete_dialog_showing_state: Rc> = Rc::new(RefCell::new(true)); @@ -111,6 +113,7 @@ fn main() { //////////////////////////////////////////////////////////////////////////////////////////////// //// GUI Entry + let entry_similar_images_minimal_size: gtk::Entry = builder.get_object("entry_similar_images_minimal_size").unwrap(); let entry_duplicate_minimal_size: gtk::Entry = builder.get_object("entry_duplicate_minimal_size").unwrap(); let entry_allowed_extensions: gtk::Entry = builder.get_object("entry_allowed_extensions").unwrap(); let entry_excluded_items: gtk::Entry = builder.get_object("entry_excluded_items").unwrap(); @@ -182,13 +185,13 @@ fn main() { let text_view_errors: gtk::TextView = builder.get_object("text_view_errors").unwrap(); //// Scrolled windows - // Main notebook let scrolled_window_duplicate_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_duplicate_finder").unwrap(); let scrolled_window_main_empty_folder_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_main_empty_folder_finder").unwrap(); let scrolled_window_main_empty_files_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_main_empty_files_finder").unwrap(); let scrolled_window_main_temporary_files_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_main_temporary_files_finder").unwrap(); let scrolled_window_big_files_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_big_files_finder").unwrap(); + let scrolled_window_similar_images_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_similar_images_finder").unwrap(); // Upper notebook let scrolled_window_included_directories: gtk::ScrolledWindow = builder.get_object("scrolled_window_included_directories").unwrap(); @@ -202,6 +205,7 @@ fn main() { EmptyFiles(EmptyFiles), BigFiles(BigFile), Temporary(Temporary), + SimilarImages(SimilarImages), } // Used for getting data from thread @@ -240,7 +244,7 @@ fn main() { let mut tree_view: gtk::TreeView = TreeView::with_model(&list_store); tree_view.get_selection().set_mode(SelectionMode::Multiple); - tree_view.get_selection().set_select_function(Some(Box::new(select_function_3column))); + tree_view.get_selection().set_select_function(Some(Box::new(select_function_duplicates))); create_tree_view_duplicates(&mut tree_view); @@ -303,6 +307,22 @@ fn main() { scrolled_window_big_files_finder.add(&tree_view); scrolled_window_big_files_finder.show_all(); } + // Similar Images + { + // TODO create maybe open button to support opening multiple files at once + let col_types: [glib::types::Type; 4] = [glib::types::Type::String, glib::types::Type::String, glib::types::Type::String, glib::types::Type::String]; + let list_store: gtk::ListStore = gtk::ListStore::new(&col_types); + + let mut tree_view: gtk::TreeView = TreeView::with_model(&list_store); + + tree_view.get_selection().set_mode(SelectionMode::Multiple); + tree_view.get_selection().set_select_function(Some(Box::new(select_function_similar_images))); + + create_tree_view_similar_images(&mut tree_view); + + scrolled_window_similar_images_finder.add(&tree_view); + scrolled_window_similar_images_finder.show_all(); + } } // Set Included Directory @@ -398,6 +418,7 @@ fn main() { "scrolled_window_main_empty_files_finder" => page = "empty_file", "scrolled_window_main_temporary_files_finder" => page = "temporary_file", "notebook_big_main_file_finder" => page = "big_file", + "notebook_main_similar_images_finder_label" => page = "similar_images", e => { panic!("Not existent page {}", e); } @@ -431,11 +452,6 @@ fn main() { // Main buttons { - assert!(notebook_main_children_names.contains(&"notebook_main_duplicate_finder_label".to_string())); - assert!(notebook_main_children_names.contains(&"scrolled_window_main_empty_folder_finder".to_string())); - assert!(notebook_main_children_names.contains(&"scrolled_window_main_empty_files_finder".to_string())); - assert!(notebook_main_children_names.contains(&"scrolled_window_main_temporary_files_finder".to_string())); - assert!(notebook_main_children_names.contains(&"notebook_big_main_file_finder".to_string())); // Search button { let entry_info = entry_info.clone(); @@ -457,7 +473,7 @@ fn main() { // Disable main notebook from any iteraction until search will end notebook_main.set_sensitive(false); - entry_info.set_text("Searching data, please wait..."); + entry_info.set_text("Searching data, it may take a while please wait..."); match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { "notebook_main_duplicate_finder_label" => { @@ -562,6 +578,29 @@ fn main() { let _ = sender.send(Message::BigFiles(bf)); }); } + + "notebook_main_similar_images_finder_label" => { + let sender = sender.clone(); + let receiver_stop = rx.clone(); + + let minimal_file_size = match entry_similar_images_minimal_size.get_text().as_str().parse::() { + Ok(t) => t, + Err(_) => 1024 * 16, // By default + }; + + // Find similar images + thread::spawn(move || { + let mut sf = SimilarImages::new(); + + sf.set_included_directory(included_directories); + sf.set_excluded_directory(excluded_directories); + sf.set_recursive_search(recursive_search); + sf.set_excluded_items(excluded_items); + sf.set_minimal_file_size(minimal_file_size); + sf.find_similar_images(Option::from(&receiver_stop)); + let _ = sender.send(Message::SimilarImages(sf)); + }); + } e => panic!("Not existent {}", e), } }); @@ -577,6 +616,7 @@ fn main() { let scrolled_window_big_files_finder = scrolled_window_big_files_finder.clone(); let scrolled_window_main_empty_files_finder = scrolled_window_main_empty_files_finder.clone(); let scrolled_window_main_temporary_files_finder = scrolled_window_main_temporary_files_finder.clone(); + let scrolled_window_similar_images_finder = scrolled_window_similar_images_finder.clone(); buttons_delete.connect_clicked(move |_| { if *shared_confirmation_dialog_delete_dialog_showing_state.borrow_mut() { @@ -799,6 +839,39 @@ fn main() { text_view_errors.get_buffer().unwrap().set_text(messages.as_str()); selection.unselect_all(); } + "notebook_main_similar_images_finder_label" => { + let tree_view = scrolled_window_similar_images_finder.get_children().get(0).unwrap().clone().downcast::().unwrap(); + let selection = tree_view.get_selection(); + + let (selection_rows, tree_model) = selection.get_selected_rows(); + let list_store = tree_model.clone().downcast::().unwrap(); + + // let new_tree_model = TreeModel::new(); // TODO - maybe create new model when inserting a new data, because this seems to be not optimal when using thousands of rows + + let mut messages: String = "".to_string(); + + // Must be deleted from end to start, because when deleting entries, TreePath(and also TreeIter) will points to invalid data + for tree_path in selection_rows.iter().rev() { + let name = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(), ColumnsBigFiles::Name as i32).get::().unwrap().unwrap(); + let path = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(), ColumnsBigFiles::Path as i32).get::().unwrap().unwrap(); + + match fs::remove_file(format!("{}/{}", path, name)) { + Ok(_) => { + list_store.remove(&list_store.get_iter(tree_path).unwrap()); + } + Err(_) => { + messages += format!( + "Failed to remove file {}/{}. It is possible that you already deleted it, because similar images shows all possible file doesn't exists or you don't have permissions.\n", + path, name + ) + .as_str() + } + } + } + + text_view_errors.get_buffer().unwrap().set_text(messages.as_str()); + selection.unselect_all(); + } e => panic!("Not existent {}", e), } }); @@ -815,18 +888,6 @@ fn main() { popover_select.set_relative_to(Some(&buttons_select)); popover_select.popup(); } - "scrolled_window_main_empty_folder_finder" => { - // Do nothing - } - "scrolled_window_main_empty_files_finder" => { - // Do nothing - } - "scrolled_window_main_temporary_files_finder" => { - // Do nothing - } - "notebook_big_main_file_finder" => { - // Do nothing - } e => panic!("Not existent {}", e), }); } @@ -840,6 +901,7 @@ fn main() { let shared_big_files_state = shared_big_files_state.clone(); let shared_temporary_files_state = shared_temporary_files_state.clone(); let shared_empty_files_state = shared_empty_files_state.clone(); + let shared_similar_images_state = shared_similar_images_state.clone(); let notebook_main = notebook_main.clone(); buttons_save_clone.connect_clicked(move |_| match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { "notebook_main_duplicate_finder_label" => { @@ -907,6 +969,19 @@ fn main() { *shared_buttons.borrow_mut().get_mut("big_file").unwrap().get_mut("save").unwrap() = false; } } + "notebook_main_similar_images_finder_label" => { + let file_name = "results_similar_images.txt"; + + let mut df = shared_similar_images_state.borrow_mut(); + df.save_results_to_file(file_name); + + entry_info.set_text(format!("Saved results to file {}", file_name).as_str()); + // Set state + { + buttons_save.hide(); + *shared_buttons.borrow_mut().get_mut("similar_images").unwrap().get_mut("save").unwrap() = false; + } + } e => panic!("Not existent {}", e), }); } @@ -1745,6 +1820,97 @@ fn main() { } } } + Message::SimilarImages(sf) => { + if sf.get_stopped_search() { + entry_info.set_text("Searching for duplicated was stopped by user"); + + //Also clear list + scrolled_window_duplicate_finder + .get_children() + .get(0) + .unwrap() + .clone() + .downcast::() + .unwrap() + .get_model() + .unwrap() + .downcast::() + .unwrap() + .clear(); + } else { + // let information = sf.get_information(); + let text_messages = sf.get_text_messages(); + + let base_images_size = sf.get_similar_images().len(); + + entry_info.set_text(format!("Found similar pictures for {} images.", base_images_size).as_str()); + + // Create GUI + { + let list_store = scrolled_window_similar_images_finder + .get_children() + .get(0) + .unwrap() + .clone() + .downcast::() + .unwrap() + .get_model() + .unwrap() + .downcast::() + .unwrap(); + list_store.clear(); + + let col_indices = [0, 1, 2, 3]; + + let vec_struct_similar = sf.get_similar_images(); + + for (index, struct_similar) in vec_struct_similar.iter().enumerate() { + // Empty at the beginning + if index != 0 { + let values: [&dyn ToValue; 4] = [&"".to_string(), &"".to_string(), &"".to_string(), &"".to_string()]; + list_store.set(&list_store.append(), &col_indices, &values); + } + // Header + let (directory, file) = split_path(&struct_similar.base_image.path); + let values: [&dyn ToValue; 4] = [ + &(get_text_from_similarity(&struct_similar.base_image.similarity).to_string()), + &file, + &directory, + &(NaiveDateTime::from_timestamp(struct_similar.base_image.modified_date as i64, 0).to_string()), + ]; + list_store.set(&list_store.append(), &col_indices, &values); + + // Meat + for similar_images in &struct_similar.similar_images { + let (directory, file) = split_path(&similar_images.path); + let values: [&dyn ToValue; 4] = [ + &(get_text_from_similarity(&similar_images.similarity).to_string()), + &file, + &directory, + &(NaiveDateTime::from_timestamp(similar_images.modified_date as i64, 0).to_string()), + ]; + list_store.set(&list_store.append(), &col_indices, &values); + } + } + + print_text_messages_to_text_view(text_messages, &text_view_errors); + } + + // Set state + { + *shared_similar_images_state.borrow_mut() = sf; + + if base_images_size > 0 { + *shared_buttons.borrow_mut().get_mut("similar_images").unwrap().get_mut("save").unwrap() = true; + *shared_buttons.borrow_mut().get_mut("similar_images").unwrap().get_mut("delete").unwrap() = true; + } else { + *shared_buttons.borrow_mut().get_mut("similar_images").unwrap().get_mut("save").unwrap() = false; + *shared_buttons.borrow_mut().get_mut("similar_images").unwrap().get_mut("delete").unwrap() = false; + } + set_buttons(&mut *shared_buttons.borrow_mut().get_mut("similar_images").unwrap(), &buttons_array, &buttons_names); + } + } + } } // Returning false here would close the receiver and have senders fail glib::Continue(true)