1
0
Fork 0
mirror of synced 2024-04-28 01:22:53 +12:00

Add similar images support for GUI (#69)

This commit is contained in:
Rafał Mikrut 2020-10-15 09:04:02 +02:00 committed by GitHub
parent 3dd203f246
commit 1178e145ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 347 additions and 24 deletions

View file

@ -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

View file

@ -1,5 +1,5 @@
# Instruction
TODO
## GUI

View file

@ -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 |

View file

@ -95,6 +95,10 @@ impl SimilarImages {
&self.text_messages
}
pub const fn get_similar_images(&self) -> &Vec<StructSimilar> {
&self.similar_vectors
}
pub const fn get_information(&self) -> &Info {
&self.information
}
@ -259,6 +263,9 @@ impl SimilarImages {
let mut new_vector: Vec<StructSimilar> = 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::<Vec<_>>();
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

View file

@ -970,6 +970,84 @@ Author: Rafał Mikrut
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkBox" id="notebook_main_similar_images_finder_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">8</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Minimal file size(in bytes)</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="entry_similar_images_minimal_size">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="max_length">15</property>
<property name="text" translatable="yes">16384</property>
<property name="caps_lock_warning">False</property>
<property name="input_purpose">number</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window_similar_images_finder">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="position">5</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Similar Images</property>
</object>
<packing>
<property name="position">5</property>
<property name="tab_fill">False</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>

View file

@ -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: &gt
text_view.get_buffer().unwrap().set_text(messages.as_str());
}
pub fn select_function_3column(_tree_selection: &gtk::TreeSelection, tree_model: &gtk::TreeModel, tree_path: &gtk::TreePath, _is_path_currently_selected: bool) -> bool {
pub fn select_function_duplicates(_tree_selection: &gtk::TreeSelection, tree_model: &gtk::TreeModel, tree_path: &gtk::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::<String>().unwrap().unwrap();
// let path = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(), ColumnsDuplicates::Path as i32).get::<String>().unwrap().unwrap();
// let modification = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(),ColumnsDuplicates::Modification as i32).get::<String>().unwrap().unwrap();
@ -297,6 +344,15 @@ pub fn select_function_3column(_tree_selection: &gtk::TreeSelection, tree_model:
true
}
pub fn select_function_similar_images(_tree_selection: &gtk::TreeSelection, tree_model: &gtk::TreeModel, tree_path: &gtk::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::<String>().unwrap().unwrap();
if path.trim() == "" {
return false;
}
true
}
pub fn set_buttons(hashmap: &mut HashMap<String, bool>, 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",
}
}

View file

@ -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<RefCell<_>> = Rc::new(RefCell::new(EmptyFiles::new()));
let shared_temporary_files_state: Rc<RefCell<_>> = Rc::new(RefCell::new(Temporary::new()));
let shared_big_files_state: Rc<RefCell<_>> = Rc::new(RefCell::new(BigFile::new()));
let shared_similar_images_state: Rc<RefCell<_>> = Rc::new(RefCell::new(SimilarImages::new()));
// State of confirmation dialogs
let shared_confirmation_dialog_delete_dialog_showing_state: Rc<RefCell<_>> = 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::<u64>() {
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::<gtk::TreeView>().unwrap();
let selection = tree_view.get_selection();
let (selection_rows, tree_model) = selection.get_selected_rows();
let list_store = tree_model.clone().downcast::<gtk::ListStore>().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::<String>().unwrap().unwrap();
let path = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(), ColumnsBigFiles::Path as i32).get::<String>().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::<gtk::TreeView>()
.unwrap()
.get_model()
.unwrap()
.downcast::<gtk::ListStore>()
.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::<gtk::TreeView>()
.unwrap()
.get_model()
.unwrap()
.downcast::<gtk::ListStore>()
.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)