From b67a98f8ca6368bb195e7457b778d3a85405c93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Mikrut?= Date: Sat, 11 Nov 2023 19:10:07 +0100 Subject: [PATCH] Similar images and proper tool type --- Cargo.lock | 1 + Changelog.md | 1 + czkawka_core/src/bad_extensions.rs | 1 + czkawka_core/src/common.rs | 1 + czkawka_core/src/common_dir_traversal.rs | 4 +- czkawka_core/src/duplicate.rs | 3 + czkawka_core/src/empty_files.rs | 1 + czkawka_core/src/empty_folder.rs | 1 + czkawka_core/src/invalid_symlinks.rs | 1 + czkawka_core/src/same_music.rs | 1 + czkawka_core/src/similar_images.rs | 2 +- krokiet/Cargo.toml | 1 + krokiet/LICENSE_MIT_CODE | 21 +++++++ krokiet/README.md | 19 ++++--- krokiet/build.rs | 2 +- krokiet/src/common.rs | 4 ++ krokiet/src/connect_progress_receiver.rs | 71 ++++++++++++++++++++---- krokiet/src/connect_scan.rs | 65 +++++++++++++++++++++- krokiet/ui/bottom_panel.slint | 1 + krokiet/ui/main_lists.slint | 8 +-- krokiet/ui/progress.slint | 1 + 21 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 krokiet/LICENSE_MIT_CODE diff --git a/Cargo.lock b/Cargo.lock index 5db9451..ecf34ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3316,6 +3316,7 @@ dependencies = [ "czkawka_core", "handsome_logger", "home", + "humansize", "log", "open", "rand", diff --git a/Changelog.md b/Changelog.md index c2b5488..f75055d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,7 @@ ### Core - Using normal crossbeam channels instead of asyncio tokio channel - [#1102](https://github.com/qarmin/czkawka/pull/1102) +- Fixed tool type when using progress of empty directories ## Version 6.1.0 - 15.10.2023r - BREAKING CHANGE - Changed cache saving method, deduplicated, optimized and simplified procedure(all files needs to be hashed again) - [#1072](https://github.com/qarmin/czkawka/pull/1072), [#1086](https://github.com/qarmin/czkawka/pull/1086) diff --git a/czkawka_core/src/bad_extensions.rs b/czkawka_core/src/bad_extensions.rs index 0e2c4e0..2b30b4d 100644 --- a/czkawka_core/src/bad_extensions.rs +++ b/czkawka_core/src/bad_extensions.rs @@ -220,6 +220,7 @@ impl BadExtensions { .allowed_extensions(self.common_data.allowed_extensions.clone()) .excluded_items(self.common_data.excluded_items.clone()) .recursive_search(self.common_data.recursive_search) + .tool_type(self.common_data.tool_type) .build() .run(); diff --git a/czkawka_core/src/common.rs b/czkawka_core/src/common.rs index 7158bdc..a3f4618 100644 --- a/czkawka_core/src/common.rs +++ b/czkawka_core/src/common.rs @@ -467,6 +467,7 @@ pub fn prepare_thread_handler_common( checking_method: CheckingMethod, tool_type: ToolType, ) -> (JoinHandle<()>, Arc, Arc, AtomicBool) { + assert_ne!(tool_type, ToolType::None, "ToolType::None should not exist"); let progress_thread_run = Arc::new(AtomicBool::new(true)); let atomic_counter = Arc::new(AtomicUsize::new(0)); let check_was_stopped = AtomicBool::new(false); diff --git a/czkawka_core/src/common_dir_traversal.rs b/czkawka_core/src/common_dir_traversal.rs index 65db633..340a012 100644 --- a/czkawka_core/src/common_dir_traversal.rs +++ b/czkawka_core/src/common_dir_traversal.rs @@ -187,7 +187,7 @@ impl<'a, 'b> DirTraversalBuilder<'a, 'b, ()> { directories: None, allowed_extensions: None, excluded_items: None, - tool_type: ToolType::BadExtensions, + tool_type: ToolType::None, } } } @@ -341,6 +341,8 @@ where { #[fun_time(message = "run(collecting files/dirs)", level = "debug")] pub fn run(self) -> DirTraversalResult { + assert!(self.tool_type != ToolType::None, "Tool type cannot be None"); + let mut all_warnings = vec![]; let mut grouped_file_entries: BTreeMap> = BTreeMap::new(); let mut folder_entries: BTreeMap = BTreeMap::new(); diff --git a/czkawka_core/src/duplicate.rs b/czkawka_core/src/duplicate.rs index a98a0db..7946fe9 100644 --- a/czkawka_core/src/duplicate.rs +++ b/czkawka_core/src/duplicate.rs @@ -169,6 +169,7 @@ impl DuplicateFinder { .recursive_search(self.common_data.recursive_search) .minimal_file_size(self.common_data.minimal_file_size) .maximal_file_size(self.common_data.maximal_file_size) + .tool_type(self.common_data.tool_type) .build() .run(); @@ -244,6 +245,7 @@ impl DuplicateFinder { .recursive_search(self.common_data.recursive_search) .minimal_file_size(self.common_data.minimal_file_size) .maximal_file_size(self.common_data.maximal_file_size) + .tool_type(self.common_data.tool_type) .build() .run(); @@ -321,6 +323,7 @@ impl DuplicateFinder { .recursive_search(self.common_data.recursive_search) .minimal_file_size(self.common_data.minimal_file_size) .maximal_file_size(self.common_data.maximal_file_size) + .tool_type(self.common_data.tool_type) .build() .run(); diff --git a/czkawka_core/src/empty_files.rs b/czkawka_core/src/empty_files.rs index 2360b9e..372ec05 100644 --- a/czkawka_core/src/empty_files.rs +++ b/czkawka_core/src/empty_files.rs @@ -63,6 +63,7 @@ impl EmptyFiles { .allowed_extensions(self.common_data.allowed_extensions.clone()) .excluded_items(self.common_data.excluded_items.clone()) .recursive_search(self.common_data.recursive_search) + .tool_type(self.common_data.tool_type) .build() .run(); diff --git a/czkawka_core/src/empty_folder.rs b/czkawka_core/src/empty_folder.rs index 73050f0..2d7a09f 100644 --- a/czkawka_core/src/empty_folder.rs +++ b/czkawka_core/src/empty_folder.rs @@ -83,6 +83,7 @@ impl EmptyFolder { .excluded_items(self.common_data.excluded_items.clone()) .collect(Collect::EmptyFolders) .max_stage(0) + .tool_type(self.common_data.tool_type) .build() .run(); diff --git a/czkawka_core/src/invalid_symlinks.rs b/czkawka_core/src/invalid_symlinks.rs index a3d036b..574af56 100644 --- a/czkawka_core/src/invalid_symlinks.rs +++ b/czkawka_core/src/invalid_symlinks.rs @@ -52,6 +52,7 @@ impl InvalidSymlinks { .allowed_extensions(self.common_data.allowed_extensions.clone()) .excluded_items(self.common_data.excluded_items.clone()) .recursive_search(self.common_data.recursive_search) + .tool_type(self.common_data.tool_type) .build() .run(); diff --git a/czkawka_core/src/same_music.rs b/czkawka_core/src/same_music.rs index 178e308..89e5fab 100644 --- a/czkawka_core/src/same_music.rs +++ b/czkawka_core/src/same_music.rs @@ -203,6 +203,7 @@ impl SameMusic { .allowed_extensions(self.common_data.allowed_extensions.clone()) .excluded_items(self.common_data.excluded_items.clone()) .recursive_search(self.common_data.recursive_search) + .tool_type(self.common_data.tool_type) .max_stage(max_stage) .build() .run(); diff --git a/czkawka_core/src/similar_images.rs b/czkawka_core/src/similar_images.rs index 43a4425..2bf403d 100644 --- a/czkawka_core/src/similar_images.rs +++ b/czkawka_core/src/similar_images.rs @@ -94,7 +94,7 @@ pub struct SimilarImages { // Hashmap with image hashes and Vector with names of files similarity: u32, images_to_check: BTreeMap, - hash_size: u8, + pub hash_size: u8, // TODO to remove pub, this is needeed by new gui, because there is no way to check what exactly was seelected hash_alg: HashAlg, image_filter: FilterType, exclude_images_with_same_size: bool, diff --git a/krokiet/Cargo.toml b/krokiet/Cargo.toml index fd88895..98010d1 100644 --- a/krokiet/Cargo.toml +++ b/krokiet/Cargo.toml @@ -31,6 +31,7 @@ home = "0.5.5" log = "0.4.20" serde = "1.0" serde_json = "1.0" +humansize = "2.1.3" [build-dependencies] slint-build = "1.3.0" diff --git a/krokiet/LICENSE_MIT_CODE b/krokiet/LICENSE_MIT_CODE new file mode 100644 index 0000000..8836e46 --- /dev/null +++ b/krokiet/LICENSE_MIT_CODE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2023 Rafał Mikrut + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/krokiet/README.md b/krokiet/README.md index f90ea89..3408eed 100644 --- a/krokiet/README.md +++ b/krokiet/README.md @@ -73,14 +73,13 @@ should print something like Slint: Build config: debug; Backend: software ``` -## Dark/Different theme +## Different theme -App was created with white fluent theme by default, but is easily possible to use dark theme by setting `SLINT_STYLE` -environment variable to `fluent-dark` during compilation -e.g. +App was created with fluent theme in mind, but is possible to use dark theme by setting `SLINT_STYLE` environment +variable to `fluent-dark` during compilation e.g. ``` -SLINT_STYLE=fluent-dark cargo run -- --path . +SLINT_STYLE=fluent-light cargo run -- --path . ``` Slint supports also other themes, but they are not officially supported by this app and may be broken. @@ -97,7 +96,8 @@ SLINT_STYLE=material-dark cargo run -- --path . - Suggesting possible design changes in the gui - of course, they should be possible to be simply implemented in the slint keeping in mind the performance aspect as well - Modifying user interface - gui is written in simple language similar to qml, that can be modified in vscode/web with - live preview - [slint live preview example](https://slint.dev/releases/1.3.0/editor/?load_demo=examples/printerdemo/ui/printerdemo.slint) + live + preview - [slint live preview example](https://slint.dev/releases/1.3.0/editor/?load_demo=examples/printerdemo/ui/printerdemo.slint) - Improving libraries used by Krokiet e.g. czkawka_core, image-rs etc. - Improving app rust code @@ -120,6 +120,8 @@ SLINT_STYLE=material-dark cargo run -- --path . - proper popup windows - slint not handle them properly - logo - about window +- reference folders +- translations(problem is only with interface, messages like "Checking {x} file" can be easily translated from rust side) ## Why Slint? @@ -145,10 +147,13 @@ errors. So only Slint left with its cons and pros. +## License +Code is licensed under MIT license but entire project is licensed under GPL-3.0 license, due Slint license restrictions. + ## Name Why Krokiet(eng. Croquette)? Because I like croquettes(Polish version), the ones with meat, mushrooms wrapped in breadcrumbs... it makes my mouth water. I considered also other dishes which I like to eat like pierogi, żurek, pączek, schabowy or zapiekanka. -This name should be a lot of easier to remember than Czkawka. \ No newline at end of file +This name should be a lot of easier to remember than czkawka or szyszka. \ No newline at end of file diff --git a/krokiet/build.rs b/krokiet/build.rs index 87696d6..7f1c722 100644 --- a/krokiet/build.rs +++ b/krokiet/build.rs @@ -1,7 +1,7 @@ use std::env; fn main() { - if env::var("SLINT_STYLE").is_err() || env::var("SLINT_STYLE") == Ok("".into()) { + if env::var("SLINT_STYLE").is_err() || env::var("SLINT_STYLE") == Ok(String::new()) { slint_build::compile_with_config("ui/main_window.slint", slint_build::CompilerConfiguration::new().with_style("fluent-dark".into())).unwrap(); } else { slint_build::compile("ui/main_window.slint").unwrap(); diff --git a/krokiet/src/common.rs b/krokiet/src/common.rs index 4b4eed7..7444fa9 100644 --- a/krokiet/src/common.rs +++ b/krokiet/src/common.rs @@ -23,3 +23,7 @@ pub fn create_string_standard_list_view_from_pathbuf(items: &[PathBuf]) -> Model .collect::>(); ModelRc::new(VecModel::from(new_folders_standard_list_view)) } + +pub fn create_vec_model_from_vec_string(items: Vec) -> VecModel { + VecModel::from(items.into_iter().map(SharedString::from).collect::>()) +} diff --git a/krokiet/src/connect_progress_receiver.rs b/krokiet/src/connect_progress_receiver.rs index 0064d95..24d32fd 100644 --- a/krokiet/src/connect_progress_receiver.rs +++ b/krokiet/src/connect_progress_receiver.rs @@ -1,7 +1,7 @@ use crate::{MainWindow, ProgressToSend}; use crossbeam_channel::Receiver; -use czkawka_core::common_dir_traversal::ProgressData; +use czkawka_core::common_dir_traversal::{ProgressData, ToolType}; use slint::{ComponentHandle, SharedString}; use std::thread; @@ -14,29 +14,80 @@ pub fn connect_progress_gathering(app: &MainWindow, progress_receiver: Receiver< }; a.upgrade_in_event_loop(move |app| { - let (all_stages, current_stage) = common_get_data(&progress_data); - let to_send = ProgressToSend { - all_progress: (all_stages * 100.0) as i32, - current_progress: (current_stage * 100.0) as i32, - step_name: SharedString::from(format!("Checked {} folders", progress_data.entries_checked)), - }; + let to_send; + match progress_data.tool_type { + ToolType::EmptyFiles => { + let (all_progress, current_progress) = no_current_stage_get_data(&progress_data); + to_send = ProgressToSend { + all_progress, + current_progress, + step_name: SharedString::from(format!("Checked {} files", progress_data.entries_checked)), + }; + } + ToolType::EmptyFolders => { + let (all_progress, current_progress) = no_current_stage_get_data(&progress_data); + to_send = ProgressToSend { + all_progress, + current_progress, + step_name: SharedString::from(format!("Checked {} folders", progress_data.entries_checked)), + }; + } + ToolType::SimilarImages => { + let step_name; + let all_progress; + let current_progress; + match progress_data.current_stage { + 0 => { + (all_progress, current_progress) = no_current_stage_get_data(&progress_data); + step_name = format!("Scanning {} file", progress_data.entries_checked); + } + 1 => { + (all_progress, current_progress) = common_get_data(&progress_data); + step_name = format!("Hashing {}/{} image", progress_data.entries_checked, progress_data.entries_to_check); + } + 2 => { + (all_progress, current_progress) = common_get_data(&progress_data); + step_name = format!("Comparing {}/{} image hash", progress_data.entries_checked, progress_data.entries_to_check); + } + _ => panic!(), + } + to_send = ProgressToSend { + all_progress, + current_progress, + step_name: SharedString::from(step_name), + }; + } + _ => { + panic!("Invalid tool type {:?}", progress_data.tool_type); + } + } app.set_progress_datas(to_send); }) .unwrap(); }); } -fn common_get_data(item: &ProgressData) -> (f64, f64) { + +// Used when current stage not have enough data to show status, so we show only all_stages +// Happens if we searching files and we don't know how many files we need to check +fn no_current_stage_get_data(item: &ProgressData) -> (i32, i32) { + let all_stages = (item.current_stage as f64) / (item.max_stage + 1) as f64; + + ((all_stages * 100.0) as i32, -1) +} + +// Used to calculate number of files to check and also to calculate current progress according to number of files to check and checked +fn common_get_data(item: &ProgressData) -> (i32, i32) { if item.entries_to_check != 0 { let all_stages = (item.current_stage as f64 + (item.entries_checked) as f64 / item.entries_to_check as f64) / (item.max_stage + 1) as f64; let all_stages = if all_stages > 0.99 { 0.99 } else { all_stages }; let current_stage = (item.entries_checked) as f64 / item.entries_to_check as f64; let current_stage = if current_stage > 0.99 { 0.99 } else { current_stage }; - (all_stages, current_stage) + ((all_stages * 100.0) as i32, (current_stage * 100.0) as i32) } else { let all_stages = (item.current_stage as f64) / (item.max_stage + 1) as f64; let all_stages = if all_stages > 0.99 { 0.99 } else { all_stages }; - (all_stages, 0f64) + ((all_stages * 100.0) as i32, 0) } } diff --git a/krokiet/src/connect_scan.rs b/krokiet/src/connect_scan.rs index 226cbdc..9f05946 100644 --- a/krokiet/src/connect_scan.rs +++ b/krokiet/src/connect_scan.rs @@ -1,3 +1,4 @@ +use crate::common::create_vec_model_from_vec_string; use crate::settings::{collect_settings, SettingsCustom}; use crate::Settings; use crate::{CurrentTab, MainListModel, MainWindow, ProgressToSend}; @@ -9,6 +10,10 @@ use czkawka_core::common_tool::CommonData; use czkawka_core::common_traits::ResultEntry; use czkawka_core::empty_files::EmptyFiles; use czkawka_core::empty_folder::EmptyFolder; +use czkawka_core::similar_images; +use czkawka_core::similar_images::SimilarImages; +use humansize::format_size; +use humansize::BINARY; use slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak}; use std::path::PathBuf; use std::rc::Rc; @@ -23,7 +28,7 @@ pub fn connect_scan_button(app: &MainWindow, progress_sender: Sender { scan_empty_files(a, progress_sender, stop_receiver, custom_settings); } - _ => panic!(), + CurrentTab::SimilarImages => { + scan_similar_images(a, progress_sender, stop_receiver, custom_settings); + } // _ => panic!(), } }); } +// TODO handle referenced folders +fn scan_similar_images(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::spawn(move || { + let mut finder = SimilarImages::new(); + finder.set_included_directory(custom_settings.included_directories.clone()); + finder.set_excluded_directory(custom_settings.excluded_directories.clone()); + finder.find_similar_images(Some(&stop_receiver), Some(&progress_sender)); + + let mut vector = finder.get_similar_images().clone(); + let messages = finder.get_text_messages().create_messages_text(); + + for vec_fe in &mut vector { + vec_fe.sort_unstable_by_key(|e| e.similarity); + } + + let hash_size = finder.hash_size; + + a.upgrade_in_event_loop(move |app| { + let number_of_empty_files = vector.len(); + let items = Rc::new(VecModel::default()); + for vec_fe in vector { + items.push(MainListModel { + checked: false, + header_row: true, + selected_row: false, + val: ModelRc::new(VecModel::default()), + }); + for fe in vec_fe { + let (directory, file) = split_path(fe.get_path()); + let data_model = create_vec_model_from_vec_string(vec![ + similar_images::get_string_from_similarity(&fe.similarity, hash_size), + format_size(fe.size, BINARY), + fe.dimensions.clone(), + file, + directory, + NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string(), + ]); + + let main = MainListModel { + checked: false, + header_row: false, + selected_row: false, + val: ModelRc::new(data_model), + }; + items.push(main); + } + } + app.set_similar_images_model(items.into()); + app.invoke_scan_ended(format!("Found {} similar images files", number_of_empty_files).into()); + app.global::().set_info_text(messages.into()); + }) + }); +} + fn scan_empty_files(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { thread::spawn(move || { let mut finder = EmptyFiles::new(); diff --git a/krokiet/ui/bottom_panel.slint b/krokiet/ui/bottom_panel.slint index 00ce526..96b7d65 100644 --- a/krokiet/ui/bottom_panel.slint +++ b/krokiet/ui/bottom_panel.slint @@ -97,6 +97,7 @@ component DirectoriesPanel { component TextErrorsPanel inherits TextEdit { height: 20px; read-only: true; + wrap: TextWrap.no-wrap; text <=> Settings.info_text; } diff --git a/krokiet/ui/main_lists.slint b/krokiet/ui/main_lists.slint index a83285f..15867f4 100644 --- a/krokiet/ui/main_lists.slint +++ b/krokiet/ui/main_lists.slint @@ -39,12 +39,12 @@ export component MainList { if root.active-tab == CurrentTab.SimilarImages: SelectableTableView { min-width: 200px; - columns: ["Selection", "File Name", "Path"]; + columns: ["Selection", "Similarity", "Size", "Dimensions", "File Name", "Path"]; last-column: "Modification Date"; - column-sizes: [35px, 100px, 350px, 100px]; + column-sizes: [35px, 80px, 80px, 80px, 350px, 100px, 100px]; values <=> similar-images-model; - parentPathIdx: 2; - fileNameIdx: 1; + parentPathIdx: 5; + fileNameIdx: 4; item_opened(item) => { item_opened(item) } diff --git a/krokiet/ui/progress.slint b/krokiet/ui/progress.slint index 6d09e4a..2c874f5 100644 --- a/krokiet/ui/progress.slint +++ b/krokiet/ui/progress.slint @@ -53,6 +53,7 @@ export component Progress { VerticalLayout { spacing: 5px; Text { + visible: progress_datas.current-progress >= -0.001; vertical-alignment: TextVerticalAlignment.center; text: progress_datas.current-progress + "%"; }