Similar images and proper tool type
This commit is contained in:
parent
9225025157
commit
b67a98f8ca
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3316,6 +3316,7 @@ dependencies = [
|
|||
"czkawka_core",
|
||||
"handsome_logger",
|
||||
"home",
|
||||
"humansize",
|
||||
"log",
|
||||
"open",
|
||||
"rand",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -467,6 +467,7 @@ pub fn prepare_thread_handler_common(
|
|||
checking_method: CheckingMethod,
|
||||
tool_type: ToolType,
|
||||
) -> (JoinHandle<()>, Arc<AtomicBool>, Arc<AtomicUsize>, 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);
|
||||
|
|
|
@ -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<T> {
|
||||
assert!(self.tool_type != ToolType::None, "Tool type cannot be None");
|
||||
|
||||
let mut all_warnings = vec![];
|
||||
let mut grouped_file_entries: BTreeMap<T, Vec<FileEntry>> = BTreeMap::new();
|
||||
let mut folder_entries: BTreeMap<PathBuf, FolderEntry> = BTreeMap::new();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -94,7 +94,7 @@ pub struct SimilarImages {
|
|||
// Hashmap with image hashes and Vector with names of files
|
||||
similarity: u32,
|
||||
images_to_check: BTreeMap<String, FileEntry>,
|
||||
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,
|
||||
|
|
|
@ -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"
|
||||
|
|
21
krokiet/LICENSE_MIT_CODE
Normal file
21
krokiet/LICENSE_MIT_CODE
Normal file
|
@ -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.
|
|
@ -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.
|
||||
This name should be a lot of easier to remember than czkawka or szyszka.
|
|
@ -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();
|
||||
|
|
|
@ -23,3 +23,7 @@ pub fn create_string_standard_list_view_from_pathbuf(items: &[PathBuf]) -> Model
|
|||
.collect::<Vec<_>>();
|
||||
ModelRc::new(VecModel::from(new_folders_standard_list_view))
|
||||
}
|
||||
|
||||
pub fn create_vec_model_from_vec_string(items: Vec<String>) -> VecModel<SharedString> {
|
||||
VecModel::from(items.into_iter().map(SharedString::from).collect::<Vec<_>>())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ProgressDat
|
|||
|
||||
app.set_progress_datas(ProgressToSend {
|
||||
all_progress: 0,
|
||||
current_progress: 0,
|
||||
current_progress: -1,
|
||||
step_name: SharedString::from(""),
|
||||
});
|
||||
|
||||
|
@ -37,11 +42,67 @@ pub fn connect_scan_button(app: &MainWindow, progress_sender: Sender<ProgressDat
|
|||
CurrentTab::EmptyFiles => {
|
||||
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<MainWindow>, progress_sender: Sender<ProgressData>, 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::<Settings>().set_info_text(messages.into());
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn scan_empty_files(a: Weak<MainWindow>, progress_sender: Sender<ProgressData>, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) {
|
||||
thread::spawn(move || {
|
||||
let mut finder = EmptyFiles::new();
|
||||
|
|
|
@ -97,6 +97,7 @@ component DirectoriesPanel {
|
|||
component TextErrorsPanel inherits TextEdit {
|
||||
height: 20px;
|
||||
read-only: true;
|
||||
wrap: TextWrap.no-wrap;
|
||||
text <=> Settings.info_text;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 + "%";
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue