diff --git a/czkawka_core/src/bad_extensions.rs b/czkawka_core/src/bad_extensions.rs index 38d1021..6d62146 100644 --- a/czkawka_core/src/bad_extensions.rs +++ b/czkawka_core/src/bad_extensions.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeSet, HashMap}; use std::io::prelude::*; use std::mem; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; @@ -169,6 +169,18 @@ pub struct BadFileEntry { pub proper_extensions: String, } +impl ResultEntry for BadFileEntry { + fn get_path(&self) -> &Path { + &self.path + } + fn get_modified_date(&self) -> u64 { + self.modified_date + } + fn get_size(&self) -> u64 { + self.size + } +} + #[derive(Default)] pub struct Info { pub number_of_files_with_bad_extension: usize, diff --git a/czkawka_core/src/common_dir_traversal.rs b/czkawka_core/src/common_dir_traversal.rs index c81dde5..0e2a663 100644 --- a/czkawka_core/src/common_dir_traversal.rs +++ b/czkawka_core/src/common_dir_traversal.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::fmt::Display; use std::fs; use std::fs::{DirEntry, FileType, Metadata}; #[cfg(target_family = "unix")] @@ -87,6 +88,15 @@ pub enum ErrorType { NonExistentFile, } +impl Display for ErrorType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ErrorType::InfiniteRecursion => write!(f, "Infinite recursion"), + ErrorType::NonExistentFile => write!(f, "Non existent file"), + } + } +} + #[derive(Copy, Clone, Eq, PartialEq)] pub enum Collect { InvalidSymlinks, @@ -318,7 +328,7 @@ 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"); + assert_ne!(self.tool_type, ToolType::None, "Tool type cannot be None"); let mut all_warnings = vec![]; let mut grouped_file_entries: BTreeMap> = BTreeMap::new(); diff --git a/czkawka_core/src/temporary.rs b/czkawka_core/src/temporary.rs index c0f16fd..649f257 100644 --- a/czkawka_core/src/temporary.rs +++ b/czkawka_core/src/temporary.rs @@ -32,11 +32,20 @@ const TEMP_EXTENSIONS: &[&str] = &[ ]; #[derive(Clone, Serialize, Debug)] -pub struct FileEntry { +pub struct TemporaryFileEntry { pub path: PathBuf, pub modified_date: u64, } +impl TemporaryFileEntry { + pub fn get_path(&self) -> &PathBuf { + &self.path + } + pub fn get_modified_date(&self) -> u64 { + self.modified_date + } +} + #[derive(Default)] pub struct Info { pub number_of_temporary_files: usize, @@ -45,7 +54,7 @@ pub struct Info { pub struct Temporary { common_data: CommonToolData, information: Info, - temporary_files: Vec, + temporary_files: Vec, } impl Temporary { @@ -138,7 +147,7 @@ impl Temporary { true } - pub fn get_file_entry(&self, atomic_counter: &Arc, entry_data: &DirEntry, warnings: &mut Vec) -> Option { + pub fn get_file_entry(&self, atomic_counter: &Arc, entry_data: &DirEntry, warnings: &mut Vec) -> Option { atomic_counter.fetch_add(1, Ordering::Relaxed); let current_file_name = entry_data.path(); @@ -158,7 +167,7 @@ impl Temporary { }; // Creating new file entry - Some(FileEntry { + Some(TemporaryFileEntry { modified_date: get_modified_time(&metadata, warnings, ¤t_file_name, false), path: current_file_name, }) @@ -234,7 +243,7 @@ impl CommonData for Temporary { } impl Temporary { - pub const fn get_temporary_files(&self) -> &Vec { + pub const fn get_temporary_files(&self) -> &Vec { &self.temporary_files } diff --git a/czkawka_gui/src/compute_results.rs b/czkawka_gui/src/compute_results.rs index 273eff7..7959d31 100644 --- a/czkawka_gui/src/compute_results.rs +++ b/czkawka_gui/src/compute_results.rs @@ -1282,7 +1282,7 @@ fn compute_duplicate_finder( fn vector_sort_unstable_entry_by_path(vector: &[T]) -> Vec where T: ResultEntry + Clone, - T: std::marker::Send, + T: Send, { if vector.len() >= 2 { let mut vector = vector.to_vec(); @@ -1296,7 +1296,7 @@ where fn vector_sort_simple_unstable_entry_by_path(vector: &[T]) -> Vec where T: ResultEntry + Clone, - T: std::marker::Send, + T: Send, { let mut vector = vector.to_vec(); vector.par_sort_unstable_by(|a, b| split_path_compare(a.get_path(), b.get_path())); diff --git a/czkawka_gui/src/connect_things/connect_button_search.rs b/czkawka_gui/src/connect_things/connect_button_search.rs index d648fb7..4108d74 100644 --- a/czkawka_gui/src/connect_things/connect_button_search.rs +++ b/czkawka_gui/src/connect_things/connect_button_search.rs @@ -77,8 +77,8 @@ pub fn connect_button_search(gui_data: &GuiData, result_sender: Sender, entry_info.set_text(&flg!("searching_for_data")); // Resets progress bars - progress_bar_all_stages.set_fraction(0 as f64); - progress_bar_current_stage.set_fraction(0 as f64); + progress_bar_all_stages.set_fraction(0f64); + progress_bar_current_stage.set_fraction(0f64); reset_text_view(&text_view_errors); @@ -162,7 +162,7 @@ impl LoadedCommonItems { .as_str() .to_string() .split(',') - .map(std::string::ToString::to_string) + .map(ToString::to_string) .collect::>(); let allowed_extensions = entry_allowed_extensions.text().as_str().to_string(); let excluded_extensions = entry_excluded_extensions.text().as_str().to_string(); diff --git a/czkawka_gui/src/connect_things/connect_progress_window.rs b/czkawka_gui/src/connect_things/connect_progress_window.rs index 09c5e08..9121842 100644 --- a/czkawka_gui/src/connect_things/connect_progress_window.rs +++ b/czkawka_gui/src/connect_things/connect_progress_window.rs @@ -217,10 +217,10 @@ fn progress_default(gui_data: &GuiData, item: &ProgressData) { fn common_set_data(item: &ProgressData, progress_bar_all_stages: &ProgressBar, progress_bar_current_stage: &ProgressBar, taskbar_state: &Rc>) { 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 = (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 }; progress_bar_all_stages.set_fraction(all_stages); - progress_bar_current_stage.set_fraction((item.entries_checked) as f64 / item.entries_to_check as f64); + progress_bar_current_stage.set_fraction(item.entries_checked as f64 / item.entries_to_check as f64); taskbar_state.borrow().set_progress_value( ((item.current_stage as usize) * item.entries_to_check + item.entries_checked) as u64, item.entries_to_check as u64 * (item.max_stage + 1) as u64, diff --git a/czkawka_gui/src/connect_things/connect_selection_of_directories.rs b/czkawka_gui/src/connect_things/connect_selection_of_directories.rs index 93074fd..daeb6cc 100644 --- a/czkawka_gui/src/connect_things/connect_selection_of_directories.rs +++ b/czkawka_gui/src/connect_things/connect_selection_of_directories.rs @@ -212,7 +212,7 @@ fn add_manually_directories(window_main: &Window, tree_view: &TreeView, excluded let list_store = get_list_store(&tree_view); if excluded_items { - if !(check_if_value_is_in_list_store(&list_store, ColumnsExcludedDirectory::Path as i32, &text)) { + if !check_if_value_is_in_list_store(&list_store, ColumnsExcludedDirectory::Path as i32, &text) { let values: [(u32, &dyn ToValue); 1] = [(ColumnsExcludedDirectory::Path as u32, &text)]; list_store.set(&list_store.append(), &values); } diff --git a/czkawka_gui/ui/about_dialog.ui b/czkawka_gui/ui/about_dialog.ui index beba11e..111524e 100644 --- a/czkawka_gui/ui/about_dialog.ui +++ b/czkawka_gui/ui/about_dialog.ui @@ -11,6 +11,6 @@ This program is free to use and will always be. mit-x11 help-about-symbolic Czkawka - 6.1.0 + 7.0.0 diff --git a/czkawka_gui/ui/czkawka.cmb b/czkawka_gui/ui/czkawka.cmb index 111df58..32aaeae 100755 --- a/czkawka_gui/ui/czkawka.cmb +++ b/czkawka_gui/ui/czkawka.cmb @@ -688,7 +688,7 @@ (5,177,"GtkWidget","focusable","1",None,None,None,None,None,None,None,None,None), (5,177,"GtkWidget","hexpand","1",None,None,None,None,None,None,None,None,None), (5,178,"GtkEditable","editable","0",None,None,None,None,None,None,None,None,None), - (5,178,"GtkEditable","text","Czkawka 6.1.0",1,None,None,None,None,None,None,None,None), + (5,178,"GtkEditable","text","Czkawka 7.0.0",1,None,None,None,None,None,None,None,None), (5,178,"GtkEditable","xalign","1",None,None,None,None,None,None,None,None,None), (5,178,"GtkEntry","has-frame","0",None,None,None,None,None,None,None,None,None), (5,178,"GtkWidget","focusable","1",None,None,None,None,None,None,None,None,None), diff --git a/czkawka_gui/ui/main_window.ui b/czkawka_gui/ui/main_window.ui index 9629ced..9395d7c 100644 --- a/czkawka_gui/ui/main_window.ui +++ b/czkawka_gui/ui/main_window.ui @@ -1160,7 +1160,7 @@ 0 1 0 - Czkawka 6.1.0 + Czkawka 7.0.0 1 diff --git a/krokiet/icons/settings.svg b/krokiet/icons/settings.svg index ac19230..64597bb 100644 --- a/krokiet/icons/settings.svg +++ b/krokiet/icons/settings.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/krokiet/icons/subsettings.svg b/krokiet/icons/subsettings.svg new file mode 100644 index 0000000..62007e6 --- /dev/null +++ b/krokiet/icons/subsettings.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/krokiet/src/common.rs b/krokiet/src/common.rs index 2243774..89c100d 100644 --- a/krokiet/src/common.rs +++ b/krokiet/src/common.rs @@ -1,63 +1,291 @@ +#![allow(dead_code)] // TODO later remove use std::path::PathBuf; use crate::{CurrentTab, ExcludedDirectoriesModel, IncludedDirectoriesModel, MainListModel, MainWindow}; use slint::{ModelRc, SharedString, VecModel}; +// Int model is used to store data in unchanged(* except that we need to split u64 into two i32) form and is used to sort/select data +// Str model is used to display data in gui + +// Duplicates +#[repr(u8)] +pub enum IntDataDuplicateFiles { + ModificationDatePart1, + ModificationDatePart2, + SizePart1, + SizePart2, +} +#[repr(u8)] +pub enum StrDataDuplicateFiles { + Size, + Name, + Path, + ModificationDate, +} + +// Empty Folders +#[repr(u8)] +pub enum IntDataEmptyFolders { + ModificationDatePart1, + ModificationDatePart2, +} + +#[repr(u8)] +pub enum StrDataEmptyFolders { + Name, + Path, + ModificationDate, +} +// Big Files +#[repr(u8)] +pub enum IntDataBigFiles { + ModificationDatePart1, + ModificationDatePart2, + SizePart1, + SizePart2, +} + +#[repr(u8)] +pub enum StrDataBigFiles { + Size, + Name, + Path, + ModificationDate, +} + +// Empty files +#[repr(u8)] +pub enum IntDataEmptyFiles { + ModificationDatePart1, + ModificationDatePart2, + SizePart1, + SizePart2, +} + +#[repr(u8)] +pub enum StrDataEmptyFiles { + Name, + Path, + ModificationDate, +} +// Temporary Files +#[repr(u8)] +pub enum IntDataTemporaryFiles { + ModificationDatePart1, + ModificationDatePart2, +} +#[repr(u8)] +pub enum StrDataTemporaryFiles { + Name, + Path, + ModificationDate, +} + +// Similar Images +#[repr(u8)] +pub enum IntDataSimilarImages { + ModificationDatePart1, + ModificationDatePart2, + SizePart1, + SizePart2, + Width, + Height, +} + +#[repr(u8)] +pub enum StrDataSimilarImages { + Similarity, + Size, + Resolution, + Name, + Path, + ModificationDate, +} + +// Similar Videos +#[repr(u8)] +pub enum IntDataSimilarVideos { + ModificationDatePart1, + ModificationDatePart2, + SizePart1, + SizePart2, +} + +#[repr(u8)] +pub enum StrDataSimilarVideos { + Size, + Name, + Path, + ModificationDate, +} + +// Similar Music +#[repr(u8)] +pub enum IntDataSimilarMusic { + ModificationDatePart1, + ModificationDatePart2, + SizePart1, + SizePart2, +} + +#[repr(u8)] +pub enum StrDataSimilarMusic { + Size, + Name, + Title, + Artist, + Year, + Bitrate, + Length, + Genre, + Path, + ModificationDate, +} + +// Invalid Symlinks +#[repr(u8)] +pub enum IntDataInvalidSymlinks { + ModificationDatePart1, + ModificationDatePart2, +} + +#[repr(u8)] +pub enum StrDataInvalidSymlinks { + SymlinkName, + SymlinkFolder, + DestinationPath, + TypeOfError, + ModificationDate, +} + +// Broken Files +#[repr(u8)] +pub enum IntDataBrokenFiles { + ModificationDatePart1, + ModificationDatePart2, + SizePart1, + SizePart2, +} + +#[repr(u8)] +pub enum StrDataBrokenFiles { + Name, + Path, + TypeOfError, + Size, + ModificationDate, +} +// Bad Extensions +#[repr(u8)] +pub enum IntDataBadExtensions { + ModificationDatePart1, + ModificationDatePart2, + SizePart1, + SizePart2, +} + +#[repr(u8)] +pub enum StrDataBadExtensions { + Name, + Path, + CurrentExtension, + ProperExtension, +} + // Remember to match updated this according to ui/main_lists.slint and connect_scan.rs files pub fn get_str_path_idx(active_tab: CurrentTab) -> usize { match active_tab { - CurrentTab::EmptyFolders => 1, - CurrentTab::EmptyFiles => 1, - CurrentTab::SimilarImages => 4, - CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::EmptyFolders => StrDataEmptyFolders::Path as usize, + CurrentTab::EmptyFiles => StrDataEmptyFiles::Path as usize, + CurrentTab::SimilarImages => StrDataSimilarImages::Path as usize, + CurrentTab::DuplicateFiles => StrDataDuplicateFiles::Path as usize, + CurrentTab::BigFiles => StrDataBigFiles::Path as usize, + CurrentTab::TemporaryFiles => StrDataTemporaryFiles::Path as usize, + CurrentTab::SimilarVideos => StrDataSimilarVideos::Path as usize, + CurrentTab::SimilarMusic => StrDataSimilarMusic::Path as usize, + CurrentTab::InvalidSymlinks => StrDataInvalidSymlinks::SymlinkFolder as usize, + CurrentTab::BrokenFiles => StrDataBrokenFiles::Path as usize, + CurrentTab::BadExtensions => StrDataBadExtensions::Path as usize, + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), } } + pub fn get_str_name_idx(active_tab: CurrentTab) -> usize { match active_tab { - CurrentTab::EmptyFolders => 0, - CurrentTab::EmptyFiles => 0, - CurrentTab::SimilarImages => 3, - CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::EmptyFolders => StrDataEmptyFolders::Name as usize, + CurrentTab::EmptyFiles => StrDataEmptyFiles::Name as usize, + CurrentTab::SimilarImages => StrDataSimilarImages::Name as usize, + CurrentTab::DuplicateFiles => StrDataDuplicateFiles::Name as usize, + CurrentTab::BigFiles => StrDataBigFiles::Name as usize, + CurrentTab::TemporaryFiles => StrDataTemporaryFiles::Name as usize, + CurrentTab::SimilarVideos => StrDataSimilarVideos::Name as usize, + CurrentTab::SimilarMusic => StrDataSimilarMusic::Name as usize, + CurrentTab::InvalidSymlinks => StrDataInvalidSymlinks::SymlinkName as usize, + CurrentTab::BrokenFiles => StrDataBrokenFiles::Name as usize, + CurrentTab::BadExtensions => StrDataBadExtensions::Name as usize, + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), } } pub fn get_int_modification_date_idx(active_tab: CurrentTab) -> usize { match active_tab { - CurrentTab::EmptyFiles => 0, - CurrentTab::SimilarImages => 0, - CurrentTab::EmptyFolders => 0, - CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::EmptyFiles => IntDataEmptyFiles::ModificationDatePart1 as usize, + CurrentTab::EmptyFolders => IntDataEmptyFolders::ModificationDatePart1 as usize, + CurrentTab::SimilarImages => IntDataSimilarImages::ModificationDatePart1 as usize, + CurrentTab::DuplicateFiles => IntDataDuplicateFiles::ModificationDatePart1 as usize, + CurrentTab::BigFiles => IntDataBigFiles::ModificationDatePart1 as usize, + CurrentTab::TemporaryFiles => IntDataTemporaryFiles::ModificationDatePart1 as usize, + CurrentTab::SimilarVideos => IntDataSimilarVideos::ModificationDatePart1 as usize, + CurrentTab::SimilarMusic => IntDataSimilarMusic::ModificationDatePart1 as usize, + CurrentTab::InvalidSymlinks => IntDataInvalidSymlinks::ModificationDatePart1 as usize, + CurrentTab::BrokenFiles => IntDataBrokenFiles::ModificationDatePart1 as usize, + CurrentTab::BadExtensions => IntDataBadExtensions::ModificationDatePart1 as usize, + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), } } + pub fn get_int_size_idx(active_tab: CurrentTab) -> usize { match active_tab { - CurrentTab::EmptyFiles => 2, - CurrentTab::SimilarImages => 2, - CurrentTab::Settings => panic!("Button should be disabled"), - CurrentTab::EmptyFolders => panic!("Unable to get size from this tab"), + CurrentTab::EmptyFiles => IntDataEmptyFiles::SizePart1 as usize, + CurrentTab::SimilarImages => IntDataSimilarImages::SizePart1 as usize, + CurrentTab::DuplicateFiles => IntDataDuplicateFiles::SizePart1 as usize, + CurrentTab::BigFiles => IntDataBigFiles::SizePart1 as usize, + CurrentTab::SimilarVideos => IntDataSimilarVideos::SizePart1 as usize, + CurrentTab::SimilarMusic => IntDataSimilarMusic::SizePart1 as usize, + CurrentTab::BrokenFiles => IntDataBrokenFiles::SizePart1 as usize, + CurrentTab::BadExtensions => IntDataBadExtensions::SizePart1 as usize, + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), + CurrentTab::EmptyFolders | CurrentTab::InvalidSymlinks | CurrentTab::TemporaryFiles => panic!("Unable to get size from this tab"), } } pub fn get_int_width_idx(active_tab: CurrentTab) -> usize { match active_tab { - CurrentTab::SimilarImages => 4, - CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::SimilarImages => IntDataSimilarImages::Width as usize, + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), _ => panic!("Unable to get height from this tab"), } } + pub fn get_int_height_idx(active_tab: CurrentTab) -> usize { match active_tab { - CurrentTab::SimilarImages => 5, - CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::SimilarImages => IntDataSimilarImages::Height as usize, + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), _ => panic!("Unable to get height from this tab"), } } pub fn get_is_header_mode(active_tab: CurrentTab) -> bool { match active_tab { - CurrentTab::EmptyFolders | CurrentTab::EmptyFiles => false, - CurrentTab::SimilarImages => true, - CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::EmptyFolders + | CurrentTab::EmptyFiles + | CurrentTab::BrokenFiles + | CurrentTab::BigFiles + | CurrentTab::TemporaryFiles + | CurrentTab::InvalidSymlinks + | CurrentTab::BadExtensions => false, + CurrentTab::SimilarImages | CurrentTab::DuplicateFiles | CurrentTab::SimilarVideos | CurrentTab::SimilarMusic => true, + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), } } @@ -66,7 +294,15 @@ pub fn get_tool_model(app: &MainWindow, tab: CurrentTab) -> ModelRc app.get_empty_folder_model(), CurrentTab::SimilarImages => app.get_similar_images_model(), CurrentTab::EmptyFiles => app.get_empty_files_model(), - CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::DuplicateFiles => app.get_duplicate_files_model(), + CurrentTab::BigFiles => app.get_big_files_model(), + CurrentTab::TemporaryFiles => app.get_temporary_files_model(), + CurrentTab::SimilarVideos => app.get_similar_videos_model(), + CurrentTab::SimilarMusic => app.get_similar_music_model(), + CurrentTab::InvalidSymlinks => app.get_invalid_symlinks_model(), + CurrentTab::BrokenFiles => app.get_broken_files_model(), + CurrentTab::BadExtensions => app.get_bad_extensions_model(), + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), } } @@ -75,7 +311,15 @@ pub fn set_tool_model(app: &MainWindow, tab: CurrentTab, model: ModelRc app.set_empty_folder_model(model), CurrentTab::SimilarImages => app.set_similar_images_model(model), CurrentTab::EmptyFiles => app.set_empty_files_model(model), - CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::DuplicateFiles => app.set_duplicate_files_model(model), + CurrentTab::BigFiles => app.set_big_files_model(model), + CurrentTab::TemporaryFiles => app.set_temporary_files_model(model), + CurrentTab::SimilarVideos => app.set_similar_videos_model(model), + CurrentTab::SimilarMusic => app.set_similar_music_model(model), + CurrentTab::InvalidSymlinks => app.set_invalid_symlinks_model(model), + CurrentTab::BrokenFiles => app.set_broken_files_model(model), + CurrentTab::BadExtensions => app.set_bad_extensions_model(model), + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), } } @@ -140,6 +384,7 @@ pub fn split_u64_into_i32s(value: u64) -> (i32, i32) { let part2: i32 = value as i32; (part1, part2) } + pub fn connect_i32_into_u64(part1: i32, part2: i32) -> u64 { ((part1 as u64) << 32) | (part2 as u64 & 0xFFFFFFFF) } @@ -155,6 +400,7 @@ mod test { assert_eq!(part1, 0); assert_eq!(part2, 1); } + #[test] fn test_split_u64_into_i32s_big() { let value = u64::MAX; @@ -162,6 +408,7 @@ mod test { assert_eq!(part1, -1); assert_eq!(part2, -1); } + #[test] fn test_connect_i32_into_u64_small() { let part1 = 0; @@ -169,6 +416,7 @@ mod test { let value = super::connect_i32_into_u64(part1, part2); assert_eq!(value, 1); } + #[test] fn test_connect_i32_into_u64_big() { let part1 = -1; @@ -176,6 +424,7 @@ mod test { let value = super::connect_i32_into_u64(part1, part2); assert_eq!(value, u64::MAX); } + #[test] fn test_connect_split_zero() { for start_value in [0, 1, 10, u32::MAX as u64, i32::MAX as u64, u64::MAX] { diff --git a/krokiet/src/connect_delete.rs b/krokiet/src/connect_delete.rs index ea5ec6d..00a5327 100644 --- a/krokiet/src/connect_delete.rs +++ b/krokiet/src/connect_delete.rs @@ -6,7 +6,7 @@ use czkawka_core::common_messages::Messages; 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}; +use crate::{Callabler, CurrentTab, GuiState, MainListModel, MainWindow, Settings}; pub fn connect_delete_button(app: &MainWindow) { let a = app.as_weak(); @@ -17,9 +17,9 @@ pub fn connect_delete_button(app: &MainWindow) { let model = get_tool_model(&app, active_tab); - let remove_to_trash = false; + let settings = app.global::(); - let (errors, new_model) = handle_delete_items(&model, active_tab, remove_to_trash); + let (errors, new_model) = handle_delete_items(&app, &model, active_tab, settings.get_move_to_trash()); if let Some(new_model) = new_model { set_tool_model(&app, active_tab, new_model); @@ -31,13 +31,14 @@ pub fn connect_delete_button(app: &MainWindow) { }); } -fn handle_delete_items(items: &ModelRc, active_tab: CurrentTab, remove_to_trash: bool) -> (Vec, Option>) { +fn handle_delete_items(app: &MainWindow, items: &ModelRc, active_tab: CurrentTab, remove_to_trash: bool) -> (Vec, Option>) { let (entries_to_delete, mut entries_left) = filter_out_checked_items(items, get_is_header_mode(active_tab)); if !entries_to_delete.is_empty() { let vec_items_to_remove = collect_full_path_from_model(&entries_to_delete, active_tab); let errors = remove_selected_items(vec_items_to_remove, active_tab, remove_to_trash); deselect_all_items(&mut entries_left); + app.set_text_summary_text(format!("Deleted {} items, failed to remove {} items", entries_to_delete.len() - errors.len(), errors.len()).into()); let r = ModelRc::new(VecModel::from(entries_left)); // TODO here maybe should also stay old model if entries cannot be removed return (errors, Some(r)); @@ -48,7 +49,6 @@ fn handle_delete_items(items: &ModelRc, active_tab: CurrentTab, r // TODO delete in parallel items, consider to add progress bar // 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_to_remove: Vec, active_tab: CurrentTab, remove_to_trash: bool) -> Vec { // Iterate over empty folders and not delete them if they are not empty if active_tab == CurrentTab::EmptyFolders { diff --git a/krokiet/src/connect_open.rs b/krokiet/src/connect_open.rs index 9165b97..eb7aa22 100644 --- a/krokiet/src/connect_open.rs +++ b/krokiet/src/connect_open.rs @@ -37,4 +37,13 @@ pub fn connect_open_items(app: &MainWindow) { error!("Failed to open cache folder {:?}: {e}", cache_folder); } }); + + app.global::().on_open_link(move |link| { + match open::that(link.as_str()) { + Ok(()) => {} + Err(e) => { + eprintln!("Failed to open link: {e}"); + } + }; + }); } diff --git a/krokiet/src/connect_progress_receiver.rs b/krokiet/src/connect_progress_receiver.rs index 230e925..efc611e 100644 --- a/krokiet/src/connect_progress_receiver.rs +++ b/krokiet/src/connect_progress_receiver.rs @@ -3,7 +3,7 @@ use std::thread; use crossbeam_channel::Receiver; use slint::ComponentHandle; -use czkawka_core::common_dir_traversal::{ProgressData, ToolType}; +use czkawka_core::common_dir_traversal::{CheckingMethod, ProgressData, ToolType}; use crate::{MainWindow, ProgressToSend}; @@ -16,60 +16,117 @@ pub fn connect_progress_gathering(app: &MainWindow, progress_receiver: Receiver< }; a.upgrade_in_event_loop(move |app| { - 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: format!("Checked {} files", progress_data.entries_checked).into(), - }; - } - ToolType::EmptyFolders => { - let (all_progress, current_progress) = no_current_stage_get_data(&progress_data); - to_send = ProgressToSend { - all_progress, - current_progress, - step_name: format!("Checked {} folders", progress_data.entries_checked).into(), - }; - } - 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!(), - } + let to_send = if progress_data.current_stage == 0 { + progress_collect_items(&progress_data, progress_data.tool_type != ToolType::EmptyFolders) + } else if check_if_loading_saving_cache(&progress_data) { + progress_save_load_cache(&progress_data) + } else { + progress_default(&progress_data) + }; - to_send = ProgressToSend { - all_progress, - current_progress, - step_name: step_name.into(), - }; - } - _ => { - panic!("Invalid tool type {:?}", progress_data.tool_type); - } - } app.set_progress_datas(to_send); }) .unwrap(); }); } +pub fn check_if_loading_saving_cache(progress_data: &ProgressData) -> bool { + matches!( + (progress_data.tool_type, progress_data.current_stage), + (ToolType::SameMusic, 1 | 3) | (ToolType::Duplicate, 1 | 3 | 4 | 6) + ) +} + +fn progress_save_load_cache(item: &ProgressData) -> ProgressToSend { + let step_name = match (item.tool_type, item.checking_method, item.current_stage) { + (ToolType::SameMusic, CheckingMethod::AudioTags | CheckingMethod::AudioContent, 1) => "Loading cache", + (ToolType::SameMusic, CheckingMethod::AudioTags | CheckingMethod::AudioContent, 3) => "Saving cache", + (ToolType::Duplicate, CheckingMethod::Hash, 1) => "Loading prehash cache", + (ToolType::Duplicate, CheckingMethod::Hash, 3) => "Saving prehash cache", + (ToolType::Duplicate, CheckingMethod::Hash, 4) => "Loading hash cache", + (ToolType::Duplicate, CheckingMethod::Hash, 6) => "Saving hash cache", + _ => unreachable!(), + }; + let (all_progress, current_progress) = common_get_data(item); + ProgressToSend { + all_progress, + current_progress, + step_name: step_name.into(), + } +} + +fn progress_collect_items(item: &ProgressData, files: bool) -> ProgressToSend { + let step_name = match (item.tool_type, item.checking_method) { + (ToolType::Duplicate, CheckingMethod::Name) => { + format!("Scanning name of {} file", item.entries_checked) + } + (ToolType::Duplicate, CheckingMethod::SizeName) => { + format!("Scanning size and name of {} file", item.entries_checked) + } + (ToolType::Duplicate, CheckingMethod::Size | CheckingMethod::Hash) => { + format!("Scanning size of {} file", item.entries_checked) + } + _ => { + if files { + format!("Scanning {} file", item.entries_checked) + } else { + format!("Scanning {} folder", item.entries_checked) + } + } + }; + let (all_progress, current_progress) = no_current_stage_get_data(item); + ProgressToSend { + all_progress, + current_progress, + step_name: step_name.into(), + } +} + +fn progress_default(item: &ProgressData) -> ProgressToSend { + let step_name = match (item.tool_type, item.checking_method, item.current_stage) { + (ToolType::SameMusic, CheckingMethod::AudioTags, 2) | (ToolType::SameMusic, CheckingMethod::AudioContent, 5) => { + format!("Checking tags of {}/{} audio file", item.entries_checked, item.entries_to_check) + } + (ToolType::SameMusic, CheckingMethod::AudioContent, 2) => { + format!("Checking content of {}/{} audio file", item.entries_checked, item.entries_to_check) + } + (ToolType::SameMusic, CheckingMethod::AudioTags, 4) => { + format!("Scanning tags of {}/{} audio file", item.entries_checked, item.entries_to_check) + } + (ToolType::SameMusic, CheckingMethod::AudioContent, 4) => { + format!("Scanning content of {}/{} audio file", item.entries_checked, item.entries_to_check) + } + (ToolType::SimilarImages, _, 1) => { + format!("Hashing of {}/{} image", item.entries_checked, item.entries_to_check) + } + (ToolType::SimilarImages, _, 2) => { + format!("Comparing {}/{} image hash", item.entries_checked, item.entries_to_check) + } + (ToolType::SimilarVideos, _, 1) => { + format!("Hashing of {}/{} video", item.entries_checked, item.entries_to_check) + } + (ToolType::BrokenFiles, _, 1) => { + format!("Checking {}/{} file", item.entries_checked, item.entries_to_check) + } + (ToolType::BadExtensions, _, 1) => { + format!("Checking {}/{} file", item.entries_checked, item.entries_to_check) + } + (ToolType::Duplicate, CheckingMethod::Hash, 2) => { + format!("Analyzing partial hash of {}/{} files", item.entries_checked, item.entries_to_check) + } + (ToolType::Duplicate, CheckingMethod::Hash, 5) => { + format!("Analyzing full hash of {}/{} files", item.entries_checked, item.entries_to_check) + } + _ => unreachable!(), + }; + let (all_progress, current_progress) = common_get_data(item); + ProgressToSend { + all_progress, + current_progress, + step_name: step_name.into(), + } +} + // 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) { @@ -81,10 +138,10 @@ fn no_current_stage_get_data(item: &ProgressData) -> (i32, i32) { // 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 = (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 = 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 * 100.0) as i32, (current_stage * 100.0) as i32) } else { diff --git a/krokiet/src/connect_scan.rs b/krokiet/src/connect_scan.rs index 3c68a3d..5da7888 100644 --- a/krokiet/src/connect_scan.rs +++ b/krokiet/src/connect_scan.rs @@ -3,21 +3,33 @@ use std::thread; use chrono::NaiveDateTime; use crossbeam_channel::{Receiver, Sender}; +use czkawka_core::bad_extensions::{BadExtensions, BadFileEntry}; +use czkawka_core::big_file::{BigFile, SearchMode}; +use czkawka_core::broken_files::{BrokenEntry, BrokenFiles, CheckedTypes}; use humansize::{format_size, BINARY}; use rayon::prelude::*; use slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak}; use czkawka_core::common::{split_path, split_path_compare, DEFAULT_THREAD_SIZE}; -use czkawka_core::common_dir_traversal::{FileEntry, ProgressData}; +use czkawka_core::common_dir_traversal::{CheckingMethod, FileEntry, ProgressData}; use czkawka_core::common_tool::CommonData; use czkawka_core::common_traits::ResultEntry; +use czkawka_core::duplicate::{DuplicateEntry, DuplicateFinder}; use czkawka_core::empty_files::EmptyFiles; use czkawka_core::empty_folder::{EmptyFolder, FolderEntry}; +use czkawka_core::invalid_symlinks::{InvalidSymlinks, SymlinksFileEntry}; +use czkawka_core::same_music::{MusicEntry, MusicSimilarity, SameMusic}; use czkawka_core::similar_images; use czkawka_core::similar_images::{ImagesEntry, SimilarImages}; +use czkawka_core::similar_videos::{SimilarVideos, VideosEntry}; +use czkawka_core::temporary::{Temporary, TemporaryFileEntry}; use crate::common::split_u64_into_i32s; -use crate::settings::{collect_settings, SettingsCustom, ALLOWED_HASH_TYPE_VALUES, ALLOWED_RESIZE_ALGORITHM_VALUES}; +use crate::settings::{ + collect_settings, get_audio_check_type_idx, get_biggest_item_idx, get_duplicates_check_method_idx, get_duplicates_hash_type_idx, get_image_hash_alg_idx, + get_resize_algorithm_idx, SettingsCustom, ALLOWED_AUDIO_CHECK_TYPE_VALUES, ALLOWED_BIG_FILE_SIZE_VALUES, ALLOWED_DUPLICATES_CHECK_METHOD_VALUES, + ALLOWED_DUPLICATES_HASH_TYPE_VALUES, ALLOWED_IMAGE_HASH_ALG_VALUES, ALLOWED_RESIZE_ALGORITHM_VALUES, +}; use crate::{CurrentTab, GuiState, MainListModel, MainWindow, ProgressToSend}; pub fn connect_scan_button(app: &MainWindow, progress_sender: Sender, stop_receiver: Receiver<()>) { @@ -37,155 +49,141 @@ pub fn connect_scan_button(app: &MainWindow, progress_sender: Sender { + scan_duplicates(a, progress_sender, stop_receiver, custom_settings); + } CurrentTab::EmptyFolders => { scan_empty_folders(a, progress_sender, stop_receiver, custom_settings); } + CurrentTab::BigFiles => { + scan_big_files(a, progress_sender, stop_receiver, custom_settings); + } CurrentTab::EmptyFiles => { scan_empty_files(a, progress_sender, stop_receiver, custom_settings); } CurrentTab::SimilarImages => { scan_similar_images(a, progress_sender, stop_receiver, custom_settings); } - CurrentTab::Settings => panic!("Button should be disabled"), + CurrentTab::SimilarVideos => { + scan_similar_videos(a, progress_sender, stop_receiver, custom_settings); + } + CurrentTab::SimilarMusic => { + scan_similar_music(a, progress_sender, stop_receiver, custom_settings); + } + CurrentTab::InvalidSymlinks => { + scan_invalid_symlinks(a, progress_sender, stop_receiver, custom_settings); + } + CurrentTab::BadExtensions => { + scan_bad_extensions(a, progress_sender, stop_receiver, custom_settings); + } + CurrentTab::BrokenFiles => { + scan_broken_files(a, progress_sender, stop_receiver, custom_settings); + } + CurrentTab::TemporaryFiles => { + scan_temporary_files(a, progress_sender, stop_receiver, custom_settings); + } + CurrentTab::Settings | CurrentTab::About => panic!("Button should be disabled"), } }); } -fn scan_similar_images(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { +// Scan Duplicates + +fn scan_duplicates(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { thread::Builder::new() .stack_size(DEFAULT_THREAD_SIZE) .spawn(move || { - let mut finder = SimilarImages::new(); + let mut finder = DuplicateFinder::new(); set_common_settings(&mut finder, &custom_settings); - finder.set_hash_size(custom_settings.similar_images_sub_hash_size); - let resize_algortithm = ALLOWED_RESIZE_ALGORITHM_VALUES - .iter() - .find(|(setting_name, _gui_name, _resize_alg)| setting_name == &custom_settings.similar_images_sub_resize_algorithm) - .expect("Resize algorithm not found") - .2; - finder.set_image_filter(resize_algortithm); - let hash_type = ALLOWED_HASH_TYPE_VALUES - .iter() - .find(|(setting_name, _gui_name, _resize_alg)| setting_name == &custom_settings.similar_images_sub_hash_type) - .expect("Hash type not found") - .2; - finder.set_hash_alg(hash_type); - dbg!(&custom_settings.similar_images_sub_ignore_same_size); - finder.set_exclude_images_with_same_size(custom_settings.similar_images_sub_ignore_same_size); - finder.set_similarity(custom_settings.similar_images_sub_similarity as u32); - finder.find_similar_images(Some(&stop_receiver), Some(&progress_sender)); - - if finder.get_use_reference() { - let mut vector = finder.get_similar_images_referenced().clone(); - let messages = finder.get_text_messages().create_messages_text(); - - let hash_size = custom_settings.similar_images_sub_hash_size; - - for (_first_entry, vec_fe) in &mut vector { - vec_fe.par_sort_unstable_by_key(|e| e.similarity); - } - - a.upgrade_in_event_loop(move |app| { - write_similar_images_results_referenced(&app, vector, messages, hash_size); - }) - } else { - let mut vector = finder.get_similar_images().clone(); - let messages = finder.get_text_messages().create_messages_text(); - - let hash_size = custom_settings.similar_images_sub_hash_size; - - for vec_fe in &mut vector { - vec_fe.par_sort_unstable_by_key(|e| e.similarity); - } - - a.upgrade_in_event_loop(move |app| { - write_similar_images_results(&app, vector, messages, hash_size); - }) - } - }) - .unwrap(); -} -fn write_similar_images_results_referenced(app: &MainWindow, vector: Vec<(ImagesEntry, Vec)>, messages: String, hash_size: u8) { - let items_found = vector.len(); - let items = Rc::new(VecModel::default()); - for (ref_fe, vec_fe) in vector { - let (data_model_str, data_model_int) = prepare_data_model_similar_images(&ref_fe, hash_size); - insert_data_to_model(&items, data_model_str, data_model_int, true); - - for fe in vec_fe { - let (data_model_str, data_model_int) = prepare_data_model_similar_images(&fe, hash_size); - insert_data_to_model(&items, data_model_str, data_model_int, false); - } - } - app.set_similar_images_model(items.into()); - app.invoke_scan_ended(format!("Found {items_found} similar images files").into()); - app.global::().set_info_text(messages.into()); -} -fn write_similar_images_results(app: &MainWindow, vector: Vec>, messages: String, hash_size: u8) { - let items_found = vector.len(); - let items = Rc::new(VecModel::default()); - for vec_fe in vector { - insert_data_to_model(&items, ModelRc::new(VecModel::default()), ModelRc::new(VecModel::default()), true); - for fe in vec_fe { - let (data_model_str, data_model_int) = prepare_data_model_similar_images(&fe, hash_size); - insert_data_to_model(&items, data_model_str, data_model_int, false); - } - } - app.set_similar_images_model(items.into()); - app.invoke_scan_ended(format!("Found {items_found} similar images files").into()); - app.global::().set_info_text(messages.into()); -} -fn prepare_data_model_similar_images(fe: &ImagesEntry, hash_size: u8) -> (ModelRc, ModelRc) { - let (directory, file) = split_path(fe.get_path()); - let data_model_str = VecModel::from_slice(&[ - similar_images::get_string_from_similarity(&fe.similarity, hash_size).into(), - format_size(fe.size, BINARY).into(), - format!("{}x{}", fe.width, fe.height).into(), - file.into(), - directory.into(), - NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(), - ]); - let modification_split = split_u64_into_i32s(fe.get_modified_date()); - let size_split = split_u64_into_i32s(fe.size); - let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1, fe.width as i32, fe.height as i32]); - (data_model_str, data_model_int) -} - -///////////////////////////////// Empty Files -fn scan_empty_files(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { - thread::Builder::new() - .stack_size(DEFAULT_THREAD_SIZE) - .spawn(move || { - let mut finder = EmptyFiles::new(); - set_common_settings(&mut finder, &custom_settings); - finder.find_empty_files(Some(&stop_receiver), Some(&progress_sender)); - - let mut vector = finder.get_empty_files().clone(); + finder.set_check_method(CheckingMethod::Hash); + finder.set_minimal_cache_file_size(custom_settings.duplicate_minimal_hash_cache_size as u64); + finder.set_minimal_prehash_cache_file_size(custom_settings.duplicate_minimal_prehash_cache_size as u64); + let check_method = ALLOWED_DUPLICATES_CHECK_METHOD_VALUES[get_duplicates_check_method_idx(&custom_settings.duplicates_sub_check_method).unwrap()].2; + finder.set_check_method(check_method); + let hash_type = ALLOWED_DUPLICATES_HASH_TYPE_VALUES[get_duplicates_hash_type_idx(&custom_settings.duplicates_sub_available_hash_type).unwrap()].2; + finder.set_hash_type(hash_type); + // finder.set_ignore_hard_links(custom_settings.ignore); // TODO + finder.set_use_prehash_cache(custom_settings.duplicate_use_prehash); + finder.set_delete_outdated_cache(custom_settings.duplicate_delete_outdated_entries); + finder.set_case_sensitive_name_comparison(custom_settings.duplicates_sub_name_case_sensitive); + finder.find_duplicates(Some(&stop_receiver), Some(&progress_sender)); let messages = finder.get_text_messages().create_messages_text(); - vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); + let mut vector; + if finder.get_use_reference() { + match finder.get_check_method() { + CheckingMethod::Hash => { + vector = finder + .get_files_with_identical_hashes_referenced() + .values() + .flatten() + .cloned() + .map(|(original, other)| (Some(original), other)) + .collect::>(); + } + CheckingMethod::Name | CheckingMethod::Size | CheckingMethod::SizeName => { + let values: Vec<_> = match finder.get_check_method() { + CheckingMethod::Name => finder.get_files_with_identical_name_referenced().values().cloned().collect(), + CheckingMethod::Size => finder.get_files_with_identical_size_referenced().values().cloned().collect(), + CheckingMethod::SizeName => finder.get_files_with_identical_size_names_referenced().values().cloned().collect(), + _ => unreachable!("Invalid check method."), + }; + vector = values.into_iter().map(|(original, other)| (Some(original), other)).collect::>(); + } + _ => unreachable!("Invalid check method."), + } + } else { + match finder.get_check_method() { + CheckingMethod::Hash => { + vector = finder.get_files_sorted_by_hash().values().flatten().cloned().map(|items| (None, items)).collect::>(); + } + CheckingMethod::Name | CheckingMethod::Size | CheckingMethod::SizeName => { + let values: Vec<_> = match finder.get_check_method() { + CheckingMethod::Name => finder.get_files_sorted_by_names().values().cloned().collect(), + CheckingMethod::Size => finder.get_files_sorted_by_size().values().cloned().collect(), + CheckingMethod::SizeName => finder.get_files_sorted_by_size_name().values().cloned().collect(), + _ => unreachable!("Invalid check method."), + }; + vector = values.into_iter().map(|items| (None, items)).collect::>(); + } + _ => unreachable!("Invalid check method."), + } + } + + for (_first, vec) in &mut vector { + vec.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); + } a.upgrade_in_event_loop(move |app| { - write_empty_files_results(&app, vector, messages); + write_duplicate_results(&app, vector, messages); }) }) .unwrap(); } -fn write_empty_files_results(app: &MainWindow, vector: Vec, messages: String) { +fn write_duplicate_results(app: &MainWindow, vector: Vec<(Option, Vec)>, messages: String) { let items_found = vector.len(); let items = Rc::new(VecModel::default()); - for fe in vector { - let (data_model_str, data_model_int) = prepare_data_model_empty_files(&fe); - insert_data_to_model(&items, data_model_str, data_model_int, false); + for (ref_fe, vec_fe) in vector { + if let Some(ref_fe) = ref_fe { + let (data_model_str, data_model_int) = prepare_data_model_duplicates(&ref_fe); + insert_data_to_model(&items, data_model_str, data_model_int, Some(true)); + } else { + insert_data_to_model(&items, ModelRc::new(VecModel::default()), ModelRc::new(VecModel::default()), Some(false)); + } + + for fe in vec_fe { + let (data_model_str, data_model_int) = prepare_data_model_duplicates(&fe); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } } - app.set_empty_files_model(items.into()); - app.invoke_scan_ended(format!("Found {items_found} empty files").into()); + app.set_duplicate_files_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} similar duplicates files").into()); app.global::().set_info_text(messages.into()); } - -fn prepare_data_model_empty_files(fe: &FileEntry) -> (ModelRc, ModelRc) { +fn prepare_data_model_duplicates(fe: &DuplicateEntry) -> (ModelRc, ModelRc) { let (directory, file) = split_path(fe.get_path()); let data_model_str = VecModel::from_slice(&[ + format_size(fe.size, BINARY).into(), file.into(), directory.into(), NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(), @@ -221,7 +219,7 @@ fn write_empty_folders_results(app: &MainWindow, vector: Vec, messa let items = Rc::new(VecModel::default()); for fe in vector { let (data_model_str, data_model_int) = prepare_data_model_empty_folders(&fe); - insert_data_to_model(&items, data_model_str, data_model_int, false); + insert_data_to_model(&items, data_model_str, data_model_int, None); } app.set_empty_folder_model(items.into()); app.invoke_scan_ended(format!("Found {items_found} empty folders").into()); @@ -240,11 +238,566 @@ fn prepare_data_model_empty_folders(fe: &FolderEntry) -> (ModelRc, (data_model_str, data_model_int) } +////////////////////////////////////////// Big files +fn scan_big_files(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::Builder::new() + .stack_size(DEFAULT_THREAD_SIZE) + .spawn(move || { + let mut finder = BigFile::new(); + set_common_settings(&mut finder, &custom_settings); + finder.set_number_of_files_to_check(custom_settings.biggest_files_sub_number_of_files as usize); + let big_files_mode = ALLOWED_BIG_FILE_SIZE_VALUES[get_biggest_item_idx(&custom_settings.biggest_files_sub_method).unwrap()].2; + finder.set_search_mode(big_files_mode); + finder.find_big_files(Some(&stop_receiver), Some(&progress_sender)); + + let mut vector = finder.get_big_files().clone(); + let messages = finder.get_text_messages().create_messages_text(); + + if big_files_mode == SearchMode::BiggestFiles { + vector.par_sort_unstable_by_key(|fe| u64::MAX - fe.size); + } else { + vector.par_sort_unstable_by_key(|fe| fe.size); + } + + a.upgrade_in_event_loop(move |app| { + write_big_files_results(&app, vector, messages); + }) + }) + .unwrap(); +} +fn write_big_files_results(app: &MainWindow, vector: Vec, messages: String) { + let items_found = vector.len(); + let items = Rc::new(VecModel::default()); + for fe in vector { + let (data_model_str, data_model_int) = prepare_data_model_big_files(&fe); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } + app.set_big_files_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} files").into()); + app.global::().set_info_text(messages.into()); +} + +fn prepare_data_model_big_files(fe: &FileEntry) -> (ModelRc, ModelRc) { + let (directory, file) = split_path(&fe.path); + let data_model_str = VecModel::from_slice(&[ + format_size(fe.size, BINARY).into(), + file.into(), + directory.into(), + NaiveDateTime::from_timestamp_opt(fe.modified_date as i64, 0).unwrap().to_string().into(), + ]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let size_split = split_u64_into_i32s(fe.size); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1]); + (data_model_str, data_model_int) +} + +///////////////////////////////// Empty Files +fn scan_empty_files(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::Builder::new() + .stack_size(DEFAULT_THREAD_SIZE) + .spawn(move || { + let mut finder = EmptyFiles::new(); + set_common_settings(&mut finder, &custom_settings); + finder.find_empty_files(Some(&stop_receiver), Some(&progress_sender)); + + let mut vector = finder.get_empty_files().clone(); + let messages = finder.get_text_messages().create_messages_text(); + + vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); + + a.upgrade_in_event_loop(move |app| { + write_empty_files_results(&app, vector, messages); + }) + }) + .unwrap(); +} +fn write_empty_files_results(app: &MainWindow, vector: Vec, messages: String) { + let items_found = vector.len(); + let items = Rc::new(VecModel::default()); + for fe in vector { + let (data_model_str, data_model_int) = prepare_data_model_empty_files(&fe); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } + app.set_empty_files_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} empty files").into()); + app.global::().set_info_text(messages.into()); +} + +fn prepare_data_model_empty_files(fe: &FileEntry) -> (ModelRc, ModelRc) { + let (directory, file) = split_path(fe.get_path()); + let data_model_str = VecModel::from_slice(&[ + file.into(), + directory.into(), + NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(), + ]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let size_split = split_u64_into_i32s(fe.size); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1]); + (data_model_str, data_model_int) +} +// Scan Similar Images + +fn scan_similar_images(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::Builder::new() + .stack_size(DEFAULT_THREAD_SIZE) + .spawn(move || { + let mut finder = SimilarImages::new(); + set_common_settings(&mut finder, &custom_settings); + + finder.set_similarity(custom_settings.similar_images_sub_similarity as u32); + let hash_alg = ALLOWED_IMAGE_HASH_ALG_VALUES[get_image_hash_alg_idx(&custom_settings.similar_images_sub_hash_alg).unwrap()].2; + finder.set_hash_alg(hash_alg); + finder.set_hash_size(custom_settings.similar_images_sub_hash_size); + let resize_algorithm = ALLOWED_RESIZE_ALGORITHM_VALUES[get_resize_algorithm_idx(&custom_settings.similar_images_sub_resize_algorithm).unwrap()].2; + finder.set_image_filter(resize_algorithm); + finder.set_delete_outdated_cache(custom_settings.similar_images_delete_outdated_entries); + finder.set_exclude_images_with_same_size(custom_settings.similar_images_sub_ignore_same_size); + + finder.find_similar_images(Some(&stop_receiver), Some(&progress_sender)); + + let messages = finder.get_text_messages().create_messages_text(); + let hash_size = custom_settings.similar_images_sub_hash_size; + + let mut vector; + if finder.get_use_reference() { + vector = finder + .get_similar_images_referenced() + .iter() + .cloned() + .map(|(original, others)| (Some(original), others)) + .collect::>(); + } else { + vector = finder.get_similar_images().iter().cloned().map(|items| (None, items)).collect::>(); + } + for (_first_entry, vec_fe) in &mut vector { + vec_fe.par_sort_unstable_by_key(|e| e.similarity); + } + + a.upgrade_in_event_loop(move |app| { + write_similar_images_results(&app, vector, messages, hash_size); + }) + }) + .unwrap(); +} +fn write_similar_images_results(app: &MainWindow, vector: Vec<(Option, Vec)>, messages: String, hash_size: u8) { + let items_found = vector.len(); + let items = Rc::new(VecModel::default()); + for (ref_fe, vec_fe) in vector { + if let Some(ref_fe) = ref_fe { + let (data_model_str, data_model_int) = prepare_data_model_similar_images(&ref_fe, hash_size); + insert_data_to_model(&items, data_model_str, data_model_int, Some(true)); + } else { + insert_data_to_model(&items, ModelRc::new(VecModel::default()), ModelRc::new(VecModel::default()), Some(false)); + } + + for fe in vec_fe { + let (data_model_str, data_model_int) = prepare_data_model_similar_images(&fe, hash_size); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } + } + app.set_similar_images_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} similar image files").into()); + app.global::().set_info_text(messages.into()); +} +fn prepare_data_model_similar_images(fe: &ImagesEntry, hash_size: u8) -> (ModelRc, ModelRc) { + let (directory, file) = split_path(fe.get_path()); + let data_model_str = VecModel::from_slice(&[ + similar_images::get_string_from_similarity(&fe.similarity, hash_size).into(), + format_size(fe.size, BINARY).into(), + format!("{}x{}", fe.width, fe.height).into(), + file.into(), + directory.into(), + NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(), + ]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let size_split = split_u64_into_i32s(fe.size); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1, fe.width as i32, fe.height as i32]); + (data_model_str, data_model_int) +} + +// Scan Similar Videos + +fn scan_similar_videos(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::Builder::new() + .stack_size(DEFAULT_THREAD_SIZE) + .spawn(move || { + let mut finder = SimilarVideos::new(); + set_common_settings(&mut finder, &custom_settings); + + finder.set_tolerance(custom_settings.similar_videos_sub_similarity); + finder.set_delete_outdated_cache(custom_settings.similar_videos_delete_outdated_entries); + finder.set_exclude_videos_with_same_size(custom_settings.similar_videos_sub_ignore_same_size); + + finder.find_similar_videos(Some(&stop_receiver), Some(&progress_sender)); + + let messages = finder.get_text_messages().create_messages_text(); + + let mut vector; + if finder.get_use_reference() { + vector = finder + .get_similar_videos_referenced() + .iter() + .cloned() + .map(|(original, others)| (Some(original), others)) + .collect::>(); + } else { + vector = finder.get_similar_videos().iter().cloned().map(|items| (None, items)).collect::>(); + } + for (_first_entry, vec_fe) in &mut vector { + vec_fe.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); + } + + a.upgrade_in_event_loop(move |app| { + write_similar_videos_results(&app, vector, messages); + }) + }) + .unwrap(); +} +fn write_similar_videos_results(app: &MainWindow, vector: Vec<(Option, Vec)>, messages: String) { + let items_found = vector.len(); + let items = Rc::new(VecModel::default()); + for (ref_fe, vec_fe) in vector { + if let Some(ref_fe) = ref_fe { + let (data_model_str, data_model_int) = prepare_data_model_similar_videos(&ref_fe); + insert_data_to_model(&items, data_model_str, data_model_int, Some(true)); + } else { + insert_data_to_model(&items, ModelRc::new(VecModel::default()), ModelRc::new(VecModel::default()), Some(false)); + } + + for fe in vec_fe { + let (data_model_str, data_model_int) = prepare_data_model_similar_videos(&fe); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } + } + app.set_similar_videos_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} similar video files").into()); + app.global::().set_info_text(messages.into()); +} +fn prepare_data_model_similar_videos(fe: &VideosEntry) -> (ModelRc, ModelRc) { + let (directory, file) = split_path(fe.get_path()); + let data_model_str = VecModel::from_slice(&[ + format_size(fe.size, BINARY).into(), + file.into(), + directory.into(), + NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(), + ]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let size_split = split_u64_into_i32s(fe.size); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1]); + (data_model_str, data_model_int) +} +// Scan Similar Music +fn scan_similar_music(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::Builder::new() + .stack_size(DEFAULT_THREAD_SIZE) + .spawn(move || { + let mut finder = SameMusic::new(); + set_common_settings(&mut finder, &custom_settings); + + let mut music_similarity: MusicSimilarity = MusicSimilarity::NONE; + if custom_settings.similar_music_sub_title { + music_similarity |= MusicSimilarity::TRACK_TITLE; + } + if custom_settings.similar_music_sub_artist { + music_similarity |= MusicSimilarity::TRACK_ARTIST; + } + if custom_settings.similar_music_sub_bitrate { + music_similarity |= MusicSimilarity::BITRATE; + } + if custom_settings.similar_music_sub_length { + music_similarity |= MusicSimilarity::LENGTH; + } + if custom_settings.similar_music_sub_year { + music_similarity |= MusicSimilarity::YEAR; + } + if custom_settings.similar_music_sub_genre { + music_similarity |= MusicSimilarity::GENRE; + } + + if music_similarity == MusicSimilarity::NONE { + a.upgrade_in_event_loop(move |app| { + app.set_text_summary_text("Cannot find similar music files without any similarity method selected.".into()); + }) + .unwrap(); + return Ok(()); + } + + finder.set_music_similarity(music_similarity); + finder.set_maximum_difference(custom_settings.similar_music_sub_maximum_difference_value as f64); + finder.set_minimum_segment_duration(custom_settings.similar_music_sub_minimal_fragment_duration_value); + let audio_check_type = ALLOWED_AUDIO_CHECK_TYPE_VALUES[get_audio_check_type_idx(&custom_settings.similar_music_sub_audio_check_type).unwrap()].2; + finder.set_check_type(audio_check_type); + finder.set_approximate_comparison(custom_settings.similar_music_sub_approximate_comparison); + + finder.find_same_music(Some(&stop_receiver), Some(&progress_sender)); + + let messages = finder.get_text_messages().create_messages_text(); + + let mut vector; + if finder.get_use_reference() { + vector = finder + .get_similar_music_referenced() + .iter() + .cloned() + .map(|(original, others)| (Some(original), others)) + .collect::>(); + } else { + vector = finder.get_duplicated_music_entries().iter().cloned().map(|items| (None, items)).collect::>(); + } + for (_first_entry, vec_fe) in &mut vector { + vec_fe.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); + } + + a.upgrade_in_event_loop(move |app| { + write_similar_music_results(&app, vector, messages); + }) + }) + .unwrap(); +} +fn write_similar_music_results(app: &MainWindow, vector: Vec<(Option, Vec)>, messages: String) { + let items_found = vector.len(); + let items = Rc::new(VecModel::default()); + for (ref_fe, vec_fe) in vector { + if let Some(ref_fe) = ref_fe { + let (data_model_str, data_model_int) = prepare_data_model_similar_music(&ref_fe); + insert_data_to_model(&items, data_model_str, data_model_int, Some(true)); + } else { + insert_data_to_model(&items, ModelRc::new(VecModel::default()), ModelRc::new(VecModel::default()), Some(false)); + } + + for fe in vec_fe { + let (data_model_str, data_model_int) = prepare_data_model_similar_music(&fe); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } + } + app.set_similar_music_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} similar music files").into()); + app.global::().set_info_text(messages.into()); +} +fn prepare_data_model_similar_music(fe: &MusicEntry) -> (ModelRc, ModelRc) { + let (directory, file) = split_path(fe.get_path()); + let data_model_str = VecModel::from_slice(&[ + format_size(fe.size, BINARY).into(), + file.into(), + fe.track_title.clone().into(), + fe.track_artist.clone().into(), + fe.year.clone().into(), + fe.bitrate.to_string().into(), + fe.length.clone().into(), + fe.genre.clone().into(), + directory.into(), + NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(), + ]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let size_split = split_u64_into_i32s(fe.size); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1]); + (data_model_str, data_model_int) +} +// Invalid Symlinks +fn scan_invalid_symlinks(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::Builder::new() + .stack_size(DEFAULT_THREAD_SIZE) + .spawn(move || { + let mut finder = InvalidSymlinks::new(); + set_common_settings(&mut finder, &custom_settings); + + finder.find_invalid_links(Some(&stop_receiver), Some(&progress_sender)); + + let mut vector = finder.get_invalid_symlinks().clone(); + let messages = finder.get_text_messages().create_messages_text(); + + vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); + + a.upgrade_in_event_loop(move |app| { + write_invalid_symlinks_results(&app, vector, messages); + }) + }) + .unwrap(); +} +fn write_invalid_symlinks_results(app: &MainWindow, vector: Vec, messages: String) { + let items_found = vector.len(); + let items = Rc::new(VecModel::default()); + for fe in vector { + let (data_model_str, data_model_int) = prepare_data_model_invalid_symlinks(&fe); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } + app.set_invalid_symlinks_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} invalid symlinks").into()); + app.global::().set_info_text(messages.into()); +} + +fn prepare_data_model_invalid_symlinks(fe: &SymlinksFileEntry) -> (ModelRc, ModelRc) { + let (directory, file) = split_path(fe.get_path()); + let data_model_str = VecModel::from_slice(&[ + file.into(), + directory.into(), + fe.symlink_info.destination_path.to_string_lossy().to_string().into(), + fe.symlink_info.type_of_error.to_string().into(), + NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(), + ]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1]); + (data_model_str, data_model_int) +} ////////////////////////////////////////// Temporary Files +fn scan_temporary_files(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::Builder::new() + .stack_size(DEFAULT_THREAD_SIZE) + .spawn(move || { + let mut finder = Temporary::new(); + set_common_settings(&mut finder, &custom_settings); + + finder.find_temporary_files(Some(&stop_receiver), Some(&progress_sender)); + + let mut vector = finder.get_temporary_files().clone(); + let messages = finder.get_text_messages().create_messages_text(); + + vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); + + a.upgrade_in_event_loop(move |app| { + write_temporary_files_results(&app, vector, messages); + }) + }) + .unwrap(); +} +fn write_temporary_files_results(app: &MainWindow, vector: Vec, messages: String) { + let items_found = vector.len(); + let items = Rc::new(VecModel::default()); + for fe in vector { + let (data_model_str, data_model_int) = prepare_data_model_temporary_files(&fe); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } + app.set_temporary_files_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} files").into()); + app.global::().set_info_text(messages.into()); +} + +fn prepare_data_model_temporary_files(fe: &TemporaryFileEntry) -> (ModelRc, ModelRc) { + let (directory, file) = split_path(&fe.path); + let data_model_str = VecModel::from_slice(&[ + file.into(), + directory.into(), + NaiveDateTime::from_timestamp_opt(fe.modified_date as i64, 0).unwrap().to_string().into(), + ]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1]); + (data_model_str, data_model_int) +} +////////////////////////////////////////// Broken Files +fn scan_broken_files(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::Builder::new() + .stack_size(DEFAULT_THREAD_SIZE) + .spawn(move || { + let mut finder = BrokenFiles::new(); + set_common_settings(&mut finder, &custom_settings); + + let mut checked_types: CheckedTypes = CheckedTypes::NONE; + if custom_settings.broken_files_sub_audio { + checked_types |= CheckedTypes::AUDIO; + } + if custom_settings.broken_files_sub_pdf { + checked_types |= CheckedTypes::PDF; + } + if custom_settings.broken_files_sub_image { + checked_types |= CheckedTypes::IMAGE; + } + if custom_settings.broken_files_sub_archive { + checked_types |= CheckedTypes::ARCHIVE; + } + + if checked_types == CheckedTypes::NONE { + a.upgrade_in_event_loop(move |app| { + app.set_text_summary_text("Cannot find broken files without any file type selected.".into()); + }) + .unwrap(); + return Ok(()); + } + + finder.set_checked_types(checked_types); + finder.find_broken_files(Some(&stop_receiver), Some(&progress_sender)); + + let mut vector = finder.get_broken_files().clone(); + let messages = finder.get_text_messages().create_messages_text(); + + vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); + + a.upgrade_in_event_loop(move |app| { + write_broken_files_results(&app, vector, messages); + }) + }) + .unwrap(); +} +fn write_broken_files_results(app: &MainWindow, vector: Vec, messages: String) { + let items_found = vector.len(); + let items = Rc::new(VecModel::default()); + for fe in vector { + let (data_model_str, data_model_int) = prepare_data_model_broken_files(&fe); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } + app.set_broken_files_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} files").into()); + app.global::().set_info_text(messages.into()); +} + +fn prepare_data_model_broken_files(fe: &BrokenEntry) -> (ModelRc, ModelRc) { + let (directory, file) = split_path(&fe.path); + let data_model_str = VecModel::from_slice(&[ + file.into(), + directory.into(), + fe.error_string.clone().into(), + format_size(fe.size, BINARY).into(), + NaiveDateTime::from_timestamp_opt(fe.modified_date as i64, 0).unwrap().to_string().into(), + ]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let size_split = split_u64_into_i32s(fe.size); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1]); + (data_model_str, data_model_int) +} +////////////////////////////////////////// Bad Extensions +fn scan_bad_extensions(a: Weak, progress_sender: Sender, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) { + thread::Builder::new() + .stack_size(DEFAULT_THREAD_SIZE) + .spawn(move || { + let mut finder = BadExtensions::new(); + set_common_settings(&mut finder, &custom_settings); + finder.find_bad_extensions_files(Some(&stop_receiver), Some(&progress_sender)); + + let mut vector = finder.get_bad_extensions_files().clone(); + let messages = finder.get_text_messages().create_messages_text(); + + vector.par_sort_unstable_by(|a, b| split_path_compare(a.path.as_path(), b.path.as_path())); + + a.upgrade_in_event_loop(move |app| { + write_bad_extensions_results(&app, vector, messages); + }) + }) + .unwrap(); +} +fn write_bad_extensions_results(app: &MainWindow, vector: Vec, messages: String) { + let items_found = vector.len(); + let items = Rc::new(VecModel::default()); + for fe in vector { + let (data_model_str, data_model_int) = prepare_data_model_bad_extensions(&fe); + insert_data_to_model(&items, data_model_str, data_model_int, None); + } + app.set_bad_extensions_model(items.into()); + app.invoke_scan_ended(format!("Found {items_found} files with bad extensions").into()); + app.global::().set_info_text(messages.into()); +} + +fn prepare_data_model_bad_extensions(fe: &BadFileEntry) -> (ModelRc, ModelRc) { + let (directory, file) = split_path(&fe.path); + let data_model_str = VecModel::from_slice(&[file.into(), directory.into(), fe.current_extension.clone().into(), fe.proper_extensions.clone().into()]); + let modification_split = split_u64_into_i32s(fe.get_modified_date()); + let size_split = split_u64_into_i32s(fe.size); + let data_model_int = VecModel::from_slice(&[modification_split.0, modification_split.1, size_split.0, size_split.1]); + (data_model_str, data_model_int) +} ////////////////////////////////////////// Common -fn insert_data_to_model(items: &Rc>, data_model_str: ModelRc, data_model_int: ModelRc, header_row: bool) { +fn insert_data_to_model(items: &Rc>, data_model_str: ModelRc, data_model_int: ModelRc, full_header_row: Option) { let main = MainListModel { checked: false, - header_row, + header_row: full_header_row.is_some(), + full_header_row: full_header_row.unwrap_or(false), selected_row: false, val_str: ModelRc::new(data_model_str), val_int: ModelRc::new(data_model_int), diff --git a/krokiet/src/connect_show_preview.rs b/krokiet/src/connect_show_preview.rs index b516709..809c082 100644 --- a/krokiet/src/connect_show_preview.rs +++ b/krokiet/src/connect_show_preview.rs @@ -1,3 +1,4 @@ +use std::panic; use std::path::Path; use std::time::{Duration, Instant}; @@ -21,7 +22,9 @@ pub fn connect_show_preview(app: &MainWindow) { let active_tab = gui_state.get_active_tab(); - if active_tab == CurrentTab::SimilarImages && !settings.get_similar_images_show_image_preview() { + if (active_tab == CurrentTab::SimilarImages && !settings.get_similar_images_show_image_preview()) + || (active_tab == CurrentTab::DuplicateFiles && !settings.get_duplicate_image_preview()) + { set_preview_visible(&gui_state, None); return; } @@ -70,7 +73,7 @@ fn convert_into_slint_image(img: DynamicImage) -> slint::Image { slint::Image::from_rgba8(buffer) } -fn load_image(image_path: &Path) -> Option<(Duration, image::DynamicImage)> { +fn load_image(image_path: &Path) -> Option<(Duration, DynamicImage)> { if !image_path.is_file() { return None; } @@ -80,30 +83,32 @@ fn load_image(image_path: &Path) -> Option<(Duration, image::DynamicImage)> { let is_raw_image = RAW_IMAGE_EXTENSIONS.contains(&image_extension.as_str()); let is_normal_image = IMAGE_RS_EXTENSIONS.contains(&image_extension.as_str()); - if !is_raw_image && !is_normal_image { - return None; - } let load_img_start_timer = Instant::now(); - // TODO this needs to be run inside closure - let img = if is_normal_image { - match image::open(image_name) { - Ok(img) => img, - Err(e) => { - error!("Error while loading image: {}", e); + let img = panic::catch_unwind(|| { + let int_img = if is_normal_image { + match image::open(image_name) { + Ok(img) => img, + Err(e) => { + error!("Error while loading image: {}", e); + return None; + } + } + } else if is_raw_image { + if let Some(img) = get_dynamic_image_from_raw_image(image_name) { + img + } else { + error!("Error while loading raw image - not sure why - try to guess"); return None; } - } - } else if is_raw_image { - if let Some(img) = get_dynamic_image_from_raw_image(image_name) { - img } else { - error!("Error while loading raw image - not sure why - try to guess"); return None; - } - } else { - panic!("Used not supported image extension"); - }; - + }; + Some(int_img) + }) + .unwrap_or_else(|e| { + error!("Error while loading image: {e:?}"); + None + })?; Some((load_img_start_timer.elapsed(), img)) } diff --git a/krokiet/src/main.rs b/krokiet/src/main.rs index b6fd86f..c9f66e2 100644 --- a/krokiet/src/main.rs +++ b/krokiet/src/main.rs @@ -64,10 +64,6 @@ fn main() { let (progress_sender, progress_receiver): (Sender, Receiver) = unbounded(); let (stop_sender, stop_receiver): (Sender<()>, Receiver<()>) = unbounded(); - // to_remove_debug(&app); - - // Slint files may already contains data in models, so clear them before starting - todo, - // check if non zeroed models are useful zeroing_all_models(&app); set_initial_gui_infos(&app); @@ -97,57 +93,12 @@ pub fn zeroing_all_models(app: &MainWindow) { app.set_empty_folder_model(Rc::new(VecModel::default()).into()); app.set_empty_files_model(Rc::new(VecModel::default()).into()); app.set_similar_images_model(Rc::new(VecModel::default()).into()); + app.set_duplicate_files_model(Rc::new(VecModel::default()).into()); + app.set_similar_music_model(Rc::new(VecModel::default()).into()); + app.set_big_files_model(Rc::new(VecModel::default()).into()); + app.set_bad_extensions_model(Rc::new(VecModel::default()).into()); + app.set_broken_files_model(Rc::new(VecModel::default()).into()); + app.set_similar_videos_model(Rc::new(VecModel::default()).into()); + app.set_invalid_symlinks_model(Rc::new(VecModel::default()).into()); + app.set_temporary_files_model(Rc::new(VecModel::default()).into()); } - -// // TODO remove this after debugging - or leave commented -// pub fn to_remove_debug(app: &MainWindow) { -// app.set_empty_folder_model(to_remove_create_without_header("@@").into()); -// app.set_empty_files_model(to_remove_create_without_header("%%").into()); -// app.set_similar_images_model(to_remove_create_with_header().into()); -// } - -// fn to_remove_create_with_header() -> Rc> { -// let header_row_data: Rc> = Rc::new(VecModel::default()); -// for r in 0..10_000 { -// let items = VecModel::default(); -// -// for c in 0..3 { -// items.push(slint::format!("Item {r}.{c}")); -// } -// -// let is_header = r % 3 == 0; -// let is_checked = (r % 2 == 0) && !is_header; -// -// let item = MainListModel { -// checked: is_checked, -// header_row: is_header, -// selected_row: false, -// val: ModelRc::new(items), -// }; -// -// header_row_data.push(item); -// } -// header_row_data -// } -// fn to_remove_create_without_header(s: &str) -> Rc> { -// let non_header_row_data: Rc> = Rc::new(VecModel::default()); -// for r in 0..100_000 { -// let items = VecModel::default(); -// -// for c in 0..3 { -// items.push(slint::format!("Item {r}.{c}.{s}")); -// } -// -// let is_checked = r % 2 == 0; -// -// let item = MainListModel { -// checked: is_checked, -// header_row: false, -// selected_row: false, -// val: ModelRc::new(items), -// }; -// -// non_header_row_data.push(item); -// } -// non_header_row_data -// } diff --git a/krokiet/src/model_operations.rs b/krokiet/src/model_operations.rs index 724855c..7ac83b6 100644 --- a/krokiet/src/model_operations.rs +++ b/krokiet/src/model_operations.rs @@ -34,8 +34,6 @@ pub fn collect_path_name_from_model(items: &[MainListModel], active_tab: Current 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(), @@ -228,6 +226,7 @@ mod tests { model.push(MainListModel { checked: item.0, header_row: item.1, + full_header_row: false, // TODO - this needs to be calculated selected_row: item.2, val_str: ModelRc::new(all_items), val_int: ModelRc::new(VecModel::default()), diff --git a/krokiet/src/set_initial_gui_info.rs b/krokiet/src/set_initial_gui_info.rs index 5543fe9..acdc481 100644 --- a/krokiet/src/set_initial_gui_info.rs +++ b/krokiet/src/set_initial_gui_info.rs @@ -2,11 +2,13 @@ use slint::{ComponentHandle, SharedString, VecModel}; use czkawka_core::common::get_all_available_threads; -use crate::settings::{ALLOWED_HASH_SIZE_VALUES, ALLOWED_HASH_TYPE_VALUES, ALLOWED_RESIZE_ALGORITHM_VALUES}; +use crate::settings::{ + ALLOWED_BIG_FILE_SIZE_VALUES, ALLOWED_DUPLICATES_CHECK_METHOD_VALUES, ALLOWED_DUPLICATES_HASH_TYPE_VALUES, ALLOWED_HASH_SIZE_VALUES, ALLOWED_IMAGE_HASH_ALG_VALUES, + ALLOWED_RESIZE_ALGORITHM_VALUES, +}; use crate::{GuiState, MainWindow, Settings}; // Some info needs to be send to gui at the start like available thread number in OS. -// pub fn set_initial_gui_infos(app: &MainWindow) { let threads = get_all_available_threads(); let settings = app.global::(); @@ -20,7 +22,19 @@ pub fn set_initial_gui_infos(app: &MainWindow) { .iter() .map(|(_settings_key, gui_name, _filter_type)| (*gui_name).into()) .collect::>(); - let available_hash_type: Vec = ALLOWED_HASH_TYPE_VALUES + let available_hash_type: Vec = ALLOWED_IMAGE_HASH_ALG_VALUES + .iter() + .map(|(_settings_key, gui_name, _hash_type)| (*gui_name).into()) + .collect::>(); + let available_big_file_search_mode: Vec = ALLOWED_BIG_FILE_SIZE_VALUES + .iter() + .map(|(_settings_key, gui_name, _search_mode)| (*gui_name).into()) + .collect::>(); + let available_duplicates_check_method: Vec = ALLOWED_DUPLICATES_CHECK_METHOD_VALUES + .iter() + .map(|(_settings_key, gui_name, _checking_method)| (*gui_name).into()) + .collect::>(); + let available_duplicates_hash_type: Vec = ALLOWED_DUPLICATES_HASH_TYPE_VALUES .iter() .map(|(_settings_key, gui_name, _hash_type)| (*gui_name).into()) .collect::>(); @@ -28,4 +42,7 @@ pub fn set_initial_gui_infos(app: &MainWindow) { settings.set_similar_images_sub_available_hash_size(VecModel::from_slice(&available_hash_size)); settings.set_similar_images_sub_available_resize_algorithm(VecModel::from_slice(&available_resize_algorithm)); settings.set_similar_images_sub_available_hash_type(VecModel::from_slice(&available_hash_type)); + settings.set_biggest_files_sub_method(VecModel::from_slice(&available_big_file_search_mode)); + settings.set_duplicates_sub_check_method(VecModel::from_slice(&available_duplicates_check_method)); + settings.set_duplicates_sub_available_hash_type(VecModel::from_slice(&available_duplicates_hash_type)); } diff --git a/krokiet/src/settings.rs b/krokiet/src/settings.rs index 7beb513..a19d689 100644 --- a/krokiet/src/settings.rs +++ b/krokiet/src/settings.rs @@ -2,15 +2,18 @@ use std::cmp::{max, min}; use std::env; use std::path::PathBuf; +use czkawka_core::big_file::SearchMode; use directories_next::ProjectDirs; use home::home_dir; use image_hasher::{FilterType, HashAlg}; use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; -use slint::{ComponentHandle, Model, ModelRc}; +use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; use czkawka_core::common::{get_all_available_threads, set_number_of_threads}; +use czkawka_core::common_dir_traversal::CheckingMethod; use czkawka_core::common_items::{DEFAULT_EXCLUDED_DIRECTORIES, DEFAULT_EXCLUDED_ITEMS}; +use czkawka_core::duplicate::HashType; use crate::common::{create_excluded_directories_model_from_pathbuf, create_included_directories_model_from_pathbuf, create_vec_model_from_vec_string}; use crate::{Callabler, GuiState, MainWindow, Settings}; @@ -19,6 +22,12 @@ pub const DEFAULT_MINIMUM_SIZE_KB: i32 = 16; pub const DEFAULT_MAXIMUM_SIZE_KB: i32 = i32::MAX / 1024; pub const DEFAULT_MINIMUM_CACHE_SIZE: i32 = 256; pub const DEFAULT_MINIMUM_PREHASH_CACHE_SIZE: i32 = 256; +pub const DEFAULT_BIGGEST_FILES: i32 = 50; +pub const DEFAULT_IMAGE_SIMILARITY: i32 = 10; +pub const DEFAULT_VIDEO_SIMILARITY: i32 = 15; +pub const DEFAULT_HASH_SIZE: u8 = 16; +pub const DEFAULT_MAXIMUM_DIFFERENCE_VALUE: f32 = 3.0; +pub const DEFAULT_MINIMAL_FRAGMENT_DURATION_VALUE: f32 = 5.0; // (Hash size, Maximum difference) - Ehh... to simplify it, just use everywhere 40 as maximum similarity - for now I'm to lazy to change it, when hash size changes // So if you want to change it, you need to change it in multiple places @@ -32,13 +41,31 @@ pub const ALLOWED_RESIZE_ALGORITHM_VALUES: &[(&str, &str, FilterType)] = &[ ("nearest", "Nearest", FilterType::Nearest), ]; -pub const ALLOWED_HASH_TYPE_VALUES: &[(&str, &str, HashAlg)] = &[ +pub const ALLOWED_IMAGE_HASH_ALG_VALUES: &[(&str, &str, HashAlg)] = &[ ("mean", "Mean", HashAlg::Mean), ("gradient", "Gradient", HashAlg::Gradient), ("blockhash", "BlockHash", HashAlg::Blockhash), ("vertgradient", "VertGradient", HashAlg::VertGradient), ("doublegradient", "DoubleGradient", HashAlg::DoubleGradient), ]; +pub const ALLOWED_BIG_FILE_SIZE_VALUES: &[(&str, &str, SearchMode)] = &[ + ("biggest", "The Biggest", SearchMode::BiggestFiles), + ("smallest", "The Smallest", SearchMode::SmallestFiles), +]; +pub const ALLOWED_AUDIO_CHECK_TYPE_VALUES: &[(&str, &str, CheckingMethod)] = + &[("tags", "Tags", CheckingMethod::AudioTags), ("fingerprint", "Fingerprint", CheckingMethod::AudioContent)]; + +pub const ALLOWED_DUPLICATES_CHECK_METHOD_VALUES: &[(&str, &str, CheckingMethod)] = &[ + ("hash", "Hash", CheckingMethod::Hash), + ("size", "Size", CheckingMethod::Size), + ("name", "Name", CheckingMethod::Name), + ("size_and_name", "Size and Name", CheckingMethod::SizeName), +]; +pub const ALLOWED_DUPLICATES_HASH_TYPE_VALUES: &[(&str, &str, HashType)] = &[ + ("blake3", "Blake3", HashType::Blake3), + ("crc32", "CRC32", HashType::Crc32), + ("xxh3", "XXH3", HashType::Xxh3), +]; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SettingsCustom { @@ -93,17 +120,55 @@ pub struct SettingsCustom { #[serde(default = "default_sub_hash_size")] pub similar_images_sub_hash_size: u8, #[serde(default = "default_hash_type")] - pub similar_images_sub_hash_type: String, + pub similar_images_sub_hash_alg: String, #[serde(default = "default_resize_algorithm")] pub similar_images_sub_resize_algorithm: String, #[serde(default)] pub similar_images_sub_ignore_same_size: bool, - #[serde(default = "default_similarity")] + #[serde(default = "default_image_similarity")] pub similar_images_sub_similarity: i32, -} - -pub fn default_similarity() -> i32 { - 10 + #[serde(default = "default_duplicates_check_method")] + pub duplicates_sub_check_method: String, + #[serde(default = "default_duplicates_hash_type")] + pub duplicates_sub_available_hash_type: String, + #[serde(default)] + pub duplicates_sub_name_case_sensitive: bool, + #[serde(default = "default_biggest_method")] + pub biggest_files_sub_method: String, + #[serde(default = "default_biggest_files")] + pub biggest_files_sub_number_of_files: i32, + #[serde(default)] + pub similar_videos_sub_ignore_same_size: bool, + #[serde(default = "default_video_similarity")] + pub similar_videos_sub_similarity: i32, + #[serde(default = "default_audio_check_type")] + pub similar_music_sub_audio_check_type: String, + #[serde(default)] + pub similar_music_sub_approximate_comparison: bool, + #[serde(default = "ttrue")] + pub similar_music_sub_title: bool, + #[serde(default = "ttrue")] + pub similar_music_sub_artist: bool, + #[serde(default)] + pub similar_music_sub_year: bool, + #[serde(default)] + pub similar_music_sub_bitrate: bool, + #[serde(default)] + pub similar_music_sub_genre: bool, + #[serde(default)] + pub similar_music_sub_length: bool, + #[serde(default = "default_maximum_difference_value")] + pub similar_music_sub_maximum_difference_value: f32, + #[serde(default = "default_minimal_fragment_duration_value")] + pub similar_music_sub_minimal_fragment_duration_value: f32, + #[serde(default = "ttrue")] + pub broken_files_sub_audio: bool, + #[serde(default)] + pub broken_files_sub_pdf: bool, + #[serde(default)] + pub broken_files_sub_archive: bool, + #[serde(default)] + pub broken_files_sub_image: bool, } impl Default for SettingsCustom { @@ -199,7 +264,7 @@ pub fn create_default_settings_files() { } } - for i in 1..=10 { + for i in 0..10 { let config_file = get_config_file(i); if let Some(config_file) = config_file { if !config_file.is_file() { @@ -377,57 +442,103 @@ pub fn set_settings_to_gui(app: &MainWindow, custom_settings: &SettingsCustom) { settings.set_duplicate_minimal_hash_cache_size(custom_settings.duplicate_minimal_hash_cache_size.to_string().into()); settings.set_duplicate_minimal_prehash_cache_size(custom_settings.duplicate_minimal_prehash_cache_size.to_string().into()); settings.set_duplicate_delete_outdated_entries(custom_settings.duplicate_delete_outdated_entries); + settings.set_duplicates_sub_name_case_sensitive(custom_settings.duplicates_sub_name_case_sensitive); settings.set_similar_images_show_image_preview(custom_settings.similar_images_show_image_preview); settings.set_similar_images_delete_outdated_entries(custom_settings.similar_images_delete_outdated_entries); settings.set_similar_videos_delete_outdated_entries(custom_settings.similar_videos_delete_outdated_entries); settings.set_similar_music_delete_outdated_entries(custom_settings.similar_music_delete_outdated_entries); - let similar_images_sub_hash_size_idx = if let Some(idx) = ALLOWED_HASH_SIZE_VALUES - .iter() - .position(|(hash_size, _max_similarity)| *hash_size == custom_settings.similar_images_sub_hash_size) - { - idx - } else { + let similar_images_sub_hash_size_idx = get_allowed_hash_size_idx(custom_settings.similar_images_sub_hash_size).unwrap_or_else(|| { warn!( "Value of hash size \"{}\" is invalid, setting it to default value", custom_settings.similar_images_sub_hash_size ); 0 - }; + }); settings.set_similar_images_sub_hash_size_index(similar_images_sub_hash_size_idx as i32); - - let similar_images_sub_hash_type_idx = if let Some(idx) = ALLOWED_HASH_TYPE_VALUES - .iter() - .position(|(settings_key, _gui_name, _hash_type)| *settings_key == custom_settings.similar_images_sub_hash_type) - { - idx - } else { + settings.set_similar_images_sub_hash_size_value(ALLOWED_HASH_SIZE_VALUES[similar_images_sub_hash_size_idx].1.to_string().into()); + // TODO all items with _value are not necessary, but due bug in slint are required, because combobox is not updated properly + let similar_images_sub_hash_alg_idx = get_image_hash_alg_idx(&custom_settings.similar_images_sub_hash_alg).unwrap_or_else(|| { warn!( "Value of hash type \"{}\" is invalid, setting it to default value", - custom_settings.similar_images_sub_hash_type + custom_settings.similar_images_sub_hash_alg ); 0 - }; - settings.set_similar_images_sub_hash_type_index(similar_images_sub_hash_type_idx as i32); - - let similar_images_sub_resize_algorithm_idx = if let Some(idx) = ALLOWED_RESIZE_ALGORITHM_VALUES - .iter() - .position(|(settings_key, _gui_name, _resize_alg)| *settings_key == custom_settings.similar_images_sub_resize_algorithm) - { - idx - } else { + }); + settings.set_similar_images_sub_hash_alg_index(similar_images_sub_hash_alg_idx as i32); + let similar_images_sub_resize_algorithm_idx = get_resize_algorithm_idx(&custom_settings.similar_images_sub_resize_algorithm).unwrap_or_else(|| { warn!( "Value of resize algorithm \"{}\" is invalid, setting it to default value", custom_settings.similar_images_sub_resize_algorithm ); 0 - }; + }); settings.set_similar_images_sub_resize_algorithm_index(similar_images_sub_resize_algorithm_idx as i32); - + settings.set_similar_images_sub_resize_algorithm_value(ALLOWED_RESIZE_ALGORITHM_VALUES[similar_images_sub_resize_algorithm_idx].1.to_string().into()); settings.set_similar_images_sub_ignore_same_size(custom_settings.similar_images_sub_ignore_same_size); - settings.set_similar_images_sub_max_similarity(40.0); // TODO this is now set to stable 40 + settings.set_similar_images_sub_max_similarity(40.0); settings.set_similar_images_sub_current_similarity(custom_settings.similar_images_sub_similarity as f32); + let duplicates_sub_check_method_idx = get_duplicates_check_method_idx(&custom_settings.duplicates_sub_check_method).unwrap_or_else(|| { + warn!( + "Value of duplicates check method \"{}\" is invalid, setting it to default value", + custom_settings.duplicates_sub_check_method + ); + 0 + }); + settings.set_duplicates_sub_check_method_index(duplicates_sub_check_method_idx as i32); + settings.set_duplicates_sub_check_method_value(ALLOWED_DUPLICATES_CHECK_METHOD_VALUES[duplicates_sub_check_method_idx].1.to_string().into()); + let duplicates_sub_available_hash_type_idx = get_duplicates_hash_type_idx(&custom_settings.duplicates_sub_available_hash_type).unwrap_or_else(|| { + warn!( + "Value of duplicates hash type \"{}\" is invalid, setting it to default value", + custom_settings.duplicates_sub_available_hash_type + ); + 0 + }); + settings.set_duplicates_sub_available_hash_type_index(duplicates_sub_available_hash_type_idx as i32); + settings.set_duplicates_sub_available_hash_type_value(ALLOWED_DUPLICATES_HASH_TYPE_VALUES[duplicates_sub_available_hash_type_idx].1.to_string().into()); + + let biggest_files_sub_method_idx = get_biggest_item_idx(&custom_settings.biggest_files_sub_method).unwrap_or_else(|| { + warn!( + "Value of biggest files method \"{}\" is invalid, setting it to default value", + custom_settings.biggest_files_sub_method + ); + 0 + }); + settings.set_biggest_files_sub_method_index(biggest_files_sub_method_idx as i32); + settings.set_biggest_files_sub_method_value(ALLOWED_BIG_FILE_SIZE_VALUES[biggest_files_sub_method_idx].1.to_string().into()); + settings.set_biggest_files_sub_number_of_files(custom_settings.biggest_files_sub_number_of_files.to_string().into()); + let all_gui_items: Vec = ALLOWED_BIG_FILE_SIZE_VALUES.iter().map(|(_, gui_name, _)| (*gui_name).into()).collect::>(); + settings.set_biggest_files_sub_method(ModelRc::new(VecModel::from(all_gui_items))); + + settings.set_similar_videos_sub_ignore_same_size(custom_settings.similar_videos_sub_ignore_same_size); + settings.set_similar_videos_sub_current_similarity(custom_settings.similar_videos_sub_similarity as f32); + settings.set_similar_videos_sub_max_similarity(20.0); + + let similar_music_sub_audio_check_type_idx = get_audio_check_type_idx(&custom_settings.similar_music_sub_audio_check_type).unwrap_or_else(|| { + warn!( + "Value of audio check type \"{}\" is invalid, setting it to default value", + custom_settings.similar_music_sub_audio_check_type + ); + 0 + }); + settings.set_similar_music_sub_audio_check_type_index(similar_music_sub_audio_check_type_idx as i32); + settings.set_similar_music_sub_audio_check_type_value(ALLOWED_AUDIO_CHECK_TYPE_VALUES[similar_music_sub_audio_check_type_idx].1.to_string().into()); + settings.set_similar_music_sub_approximate_comparison(custom_settings.similar_music_sub_approximate_comparison); + settings.set_similar_music_sub_title(custom_settings.similar_music_sub_title); + settings.set_similar_music_sub_artist(custom_settings.similar_music_sub_artist); + settings.set_similar_music_sub_year(custom_settings.similar_music_sub_year); + settings.set_similar_music_sub_bitrate(custom_settings.similar_music_sub_bitrate); + settings.set_similar_music_sub_genre(custom_settings.similar_music_sub_genre); + settings.set_similar_music_sub_length(custom_settings.similar_music_sub_length); + settings.set_similar_music_sub_maximum_difference_value(custom_settings.similar_music_sub_maximum_difference_value); + settings.set_similar_music_sub_minimal_fragment_duration_value(custom_settings.similar_music_sub_minimal_fragment_duration_value); + + settings.set_broken_files_sub_audio(custom_settings.broken_files_sub_audio); + settings.set_broken_files_sub_pdf(custom_settings.broken_files_sub_pdf); + settings.set_broken_files_sub_archive(custom_settings.broken_files_sub_archive); + settings.set_broken_files_sub_image(custom_settings.broken_files_sub_image); + // Clear text app.global::().set_info_text("".into()); } @@ -468,6 +579,7 @@ pub fn collect_settings(app: &MainWindow) -> SettingsCustom { .parse::() .unwrap_or(DEFAULT_MINIMUM_PREHASH_CACHE_SIZE); let duplicate_delete_outdated_entries = settings.get_duplicate_delete_outdated_entries(); + let duplicates_sub_name_case_sensitive = settings.get_duplicates_sub_name_case_sensitive(); let similar_images_show_image_preview = settings.get_similar_images_show_image_preview(); let similar_images_delete_outdated_entries = settings.get_similar_images_delete_outdated_entries(); @@ -478,15 +590,42 @@ pub fn collect_settings(app: &MainWindow) -> SettingsCustom { let similar_images_sub_hash_size_idx = settings.get_similar_images_sub_hash_size_index(); let similar_images_sub_hash_size = ALLOWED_HASH_SIZE_VALUES[similar_images_sub_hash_size_idx as usize].0; - - let similar_images_sub_hash_type_idx = settings.get_similar_images_sub_hash_type_index(); - let similar_images_sub_hash_type = ALLOWED_HASH_TYPE_VALUES[similar_images_sub_hash_type_idx as usize].0.to_string(); - + let similar_images_sub_hash_alg_idx = settings.get_similar_images_sub_hash_alg_index(); + let similar_images_sub_hash_alg = ALLOWED_IMAGE_HASH_ALG_VALUES[similar_images_sub_hash_alg_idx as usize].0.to_string(); let similar_images_sub_resize_algorithm_idx = settings.get_similar_images_sub_resize_algorithm_index(); let similar_images_sub_resize_algorithm = ALLOWED_RESIZE_ALGORITHM_VALUES[similar_images_sub_resize_algorithm_idx as usize].0.to_string(); - let similar_images_sub_ignore_same_size = settings.get_similar_images_sub_ignore_same_size(); let similar_images_sub_similarity = settings.get_similar_images_sub_current_similarity().round() as i32; + + let duplicates_sub_check_method_idx = settings.get_duplicates_sub_check_method_index(); + let duplicates_sub_check_method = ALLOWED_DUPLICATES_CHECK_METHOD_VALUES[duplicates_sub_check_method_idx as usize].0.to_string(); + let duplicates_sub_available_hash_type_idx = settings.get_duplicates_sub_available_hash_type_index(); + let duplicates_sub_available_hash_type = ALLOWED_DUPLICATES_HASH_TYPE_VALUES[duplicates_sub_available_hash_type_idx as usize].0.to_string(); + + let biggest_files_sub_method_idx = settings.get_biggest_files_sub_method_index(); + let biggest_files_sub_method = ALLOWED_BIG_FILE_SIZE_VALUES[biggest_files_sub_method_idx as usize].0.to_string(); + let biggest_files_sub_number_of_files = settings.get_biggest_files_sub_number_of_files().parse().unwrap_or(DEFAULT_BIGGEST_FILES); + + let similar_videos_sub_ignore_same_size = settings.get_similar_videos_sub_ignore_same_size(); + let similar_videos_sub_similarity = settings.get_similar_videos_sub_current_similarity().round() as i32; + + let similar_music_sub_audio_check_type_idx = settings.get_similar_music_sub_audio_check_type_index(); + let similar_music_sub_audio_check_type = ALLOWED_AUDIO_CHECK_TYPE_VALUES[similar_music_sub_audio_check_type_idx as usize].0.to_string(); + let similar_music_sub_approximate_comparison = settings.get_similar_music_sub_approximate_comparison(); + let similar_music_sub_title = settings.get_similar_music_sub_title(); + let similar_music_sub_artist = settings.get_similar_music_sub_artist(); + let similar_music_sub_year = settings.get_similar_music_sub_year(); + let similar_music_sub_bitrate = settings.get_similar_music_sub_bitrate(); + let similar_music_sub_genre = settings.get_similar_music_sub_genre(); + let similar_music_sub_length = settings.get_similar_music_sub_length(); + let similar_music_sub_maximum_difference_value = settings.get_similar_music_sub_maximum_difference_value(); + let similar_music_sub_minimal_fragment_duration_value = settings.get_similar_music_sub_minimal_fragment_duration_value(); + + let broken_files_sub_audio = settings.get_broken_files_sub_audio(); + let broken_files_sub_pdf = settings.get_broken_files_sub_pdf(); + let broken_files_sub_archive = settings.get_broken_files_sub_archive(); + let broken_files_sub_image = settings.get_broken_files_sub_image(); + SettingsCustom { included_directories, included_directories_referenced, @@ -513,10 +652,31 @@ pub fn collect_settings(app: &MainWindow) -> SettingsCustom { similar_videos_delete_outdated_entries, similar_music_delete_outdated_entries, similar_images_sub_hash_size, - similar_images_sub_hash_type, + similar_images_sub_hash_alg, similar_images_sub_resize_algorithm, similar_images_sub_ignore_same_size, similar_images_sub_similarity, + duplicates_sub_check_method, + duplicates_sub_available_hash_type, + duplicates_sub_name_case_sensitive, + biggest_files_sub_method, + biggest_files_sub_number_of_files, + similar_videos_sub_ignore_same_size, + similar_videos_sub_similarity, + similar_music_sub_audio_check_type, + similar_music_sub_approximate_comparison, + similar_music_sub_title, + similar_music_sub_artist, + similar_music_sub_year, + similar_music_sub_bitrate, + similar_music_sub_genre, + similar_music_sub_length, + similar_music_sub_maximum_difference_value, + similar_music_sub_minimal_fragment_duration_value, + broken_files_sub_audio, + broken_files_sub_pdf, + broken_files_sub_archive, + broken_files_sub_image, } } @@ -555,7 +715,34 @@ fn default_excluded_directories() -> Vec { excluded_directories.sort(); excluded_directories } +fn default_duplicates_check_method() -> String { + ALLOWED_DUPLICATES_CHECK_METHOD_VALUES[0].0.to_string() +} +fn default_maximum_difference_value() -> f32 { + DEFAULT_MAXIMUM_DIFFERENCE_VALUE +} +fn default_minimal_fragment_duration_value() -> f32 { + DEFAULT_MINIMAL_FRAGMENT_DURATION_VALUE +} +fn default_duplicates_hash_type() -> String { + ALLOWED_DUPLICATES_HASH_TYPE_VALUES[0].0.to_string() +} +fn default_biggest_method() -> String { + ALLOWED_BIG_FILE_SIZE_VALUES[0].0.to_string() +} +fn default_audio_check_type() -> String { + ALLOWED_AUDIO_CHECK_TYPE_VALUES[0].0.to_string() +} +fn default_video_similarity() -> i32 { + DEFAULT_VIDEO_SIMILARITY +} +fn default_biggest_files() -> i32 { + DEFAULT_BIGGEST_FILES +} +pub fn default_image_similarity() -> i32 { + DEFAULT_IMAGE_SIMILARITY +} fn default_excluded_items() -> String { DEFAULT_EXCLUDED_ITEMS.to_string() } @@ -588,8 +775,44 @@ pub fn default_resize_algorithm() -> String { ALLOWED_RESIZE_ALGORITHM_VALUES[0].0.to_string() } pub fn default_hash_type() -> String { - ALLOWED_HASH_TYPE_VALUES[0].0.to_string() + ALLOWED_IMAGE_HASH_ALG_VALUES[0].0.to_string() } pub fn default_sub_hash_size() -> u8 { - 16 + DEFAULT_HASH_SIZE +} + +fn get_allowed_hash_size_idx(h_size: u8) -> Option { + ALLOWED_HASH_SIZE_VALUES.iter().position(|(hash_size, _max_similarity)| *hash_size == h_size) +} + +pub fn get_image_hash_alg_idx(string_hash_type: &str) -> Option { + ALLOWED_IMAGE_HASH_ALG_VALUES + .iter() + .position(|(settings_key, gui_name, _hash_type)| *settings_key == string_hash_type || *gui_name == string_hash_type) +} +pub fn get_resize_algorithm_idx(string_resize_algorithm: &str) -> Option { + ALLOWED_RESIZE_ALGORITHM_VALUES + .iter() + .position(|(settings_key, gui_name, _resize_alg)| *settings_key == string_resize_algorithm || *gui_name == string_resize_algorithm) +} +pub fn get_biggest_item_idx(string_biggest_item: &str) -> Option { + ALLOWED_BIG_FILE_SIZE_VALUES + .iter() + .position(|(settings_key, gui_name, _search_mode)| *settings_key == string_biggest_item || *gui_name == string_biggest_item) +} + +pub fn get_duplicates_check_method_idx(string_duplicates_check_method: &str) -> Option { + ALLOWED_DUPLICATES_CHECK_METHOD_VALUES + .iter() + .position(|(settings_key, gui_name, _check_method)| *settings_key == string_duplicates_check_method || *gui_name == string_duplicates_check_method) +} +pub fn get_duplicates_hash_type_idx(string_duplicates_hash_type: &str) -> Option { + ALLOWED_DUPLICATES_HASH_TYPE_VALUES + .iter() + .position(|(settings_key, gui_name, _hash_type)| *settings_key == string_duplicates_hash_type || *gui_name == string_duplicates_hash_type) +} +pub fn get_audio_check_type_idx(string_audio_check_type: &str) -> Option { + ALLOWED_AUDIO_CHECK_TYPE_VALUES + .iter() + .position(|(settings_key, gui_name, _audio_check_type)| *settings_key == string_audio_check_type || *gui_name == string_audio_check_type) } diff --git a/krokiet/ui/about.slint b/krokiet/ui/about.slint new file mode 100644 index 0000000..7453986 --- /dev/null +++ b/krokiet/ui/about.slint @@ -0,0 +1,67 @@ +import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, ScrollView, LineEdit, SpinBox, ComboBox, TextEdit, Slider} from "std-widgets.slint"; +import { Settings } from "settings.slint"; +import { Callabler } from "callabler.slint"; +import { GuiState } from "gui_state.slint"; + +export component About inherits VerticalLayout { + preferred-height: 300px; + preferred-width: 400px; + + img := Image { + source: @image-url("../icons/logo.png"); + image-fit: ImageFit.contain; + } + Text { + text: "7.0.0"; + horizontal-alignment: center; + font-size: max(min(img.width / 20, 17px), 10px); + } + + VerticalLayout { + spacing: 10px; + padding-bottom: 10px; + Text { + text: "2020 - 2024 RafaƂ Mikrut(qarmin)"; + horizontal-alignment: center; + font-size: 15px; + } + Text { + text: "This program is free to use and will always be.\nSee the The MIT/GPL License for details."; + horizontal-alignment: center; + font-size: 13px; + } + Text { + text: "You may not look at unicorn, but unicorn always looks at you."; + horizontal-alignment: center; + font-size: 13px; + } + } + + HorizontalLayout { + spacing: 5px; + Button { + text: "Repository"; + clicked => { + Callabler.open_link("https://github.com/qarmin/czkawka"); + } + } + Button { + text: "Instruction"; + clicked => { + Callabler.open_link("https://github.com/qarmin/czkawka/blob/master/instructions/Instruction.md"); + } + } + Button { + text: "Donation"; + clicked => { + Callabler.open_link("https://github.com/sponsors/qarmin"); + } + } + Button { + text: "Translation"; + clicked => { + Callabler.open_link("https://crwd.in/czkawka"); + } + } + } +} \ No newline at end of file diff --git a/krokiet/ui/action_buttons.slint b/krokiet/ui/action_buttons.slint index f00ed0c..3809fa4 100644 --- a/krokiet/ui/action_buttons.slint +++ b/krokiet/ui/action_buttons.slint @@ -23,10 +23,10 @@ export component ActionButtons inherits HorizontalLayout { callback show_select_popup(length, length); callback show_remove_popup(); callback request_folder_to_move(); - in-out property bottom_panel_visibility: BottomPanelVisibility.Directories; + in-out property bottom_panel_visibility <=> GuiState.bottom_panel_visibility; in-out property stop_requested: false; in-out property scanning; - in-out property lists_enabled: GuiState.active_tab != CurrentTab.Settings; + in-out property lists_enabled: GuiState.is_tool_tab_active; out property name; height: 30px; spacing: 4px; @@ -35,7 +35,7 @@ export component ActionButtons inherits HorizontalLayout { scan_button := Button { height: parent.height; enabled: !scanning && lists_enabled; - visible: !scanning; + visible: !scanning && lists_enabled; text: "Scan"; clicked => { root.scanning = true; @@ -60,6 +60,7 @@ export component ActionButtons inherits HorizontalLayout { } move_button := Button { + visible: lists_enabled; height: parent.height; enabled: !scanning && lists_enabled; text: "Move"; @@ -69,6 +70,7 @@ export component ActionButtons inherits HorizontalLayout { } select_button := Button { + visible: lists_enabled; height: parent.height; enabled: !scanning && lists_enabled; text: "Select"; @@ -78,6 +80,7 @@ export component ActionButtons inherits HorizontalLayout { } delete_button := Button { + visible: lists_enabled; height: parent.height; enabled: !scanning && lists_enabled; text: "Delete"; diff --git a/krokiet/ui/callabler.slint b/krokiet/ui/callabler.slint index 33cd9a6..edff596 100644 --- a/krokiet/ui/callabler.slint +++ b/krokiet/ui/callabler.slint @@ -32,4 +32,6 @@ export global Callabler { callback open_config_folder(); callback open_cache_folder(); + + callback open_link(string); } diff --git a/krokiet/ui/common.slint b/krokiet/ui/common.slint index 97b41b4..0145755 100644 --- a/krokiet/ui/common.slint +++ b/krokiet/ui/common.slint @@ -1,8 +1,17 @@ export enum CurrentTab { + DuplicateFiles, EmptyFolders, + BigFiles, EmptyFiles, + TemporaryFiles, SimilarImages, - Settings + SimilarVideos, + SimilarMusic, + InvalidSymlinks, + BrokenFiles, + BadExtensions, + Settings, + About } export enum TypeOfOpenedItem { @@ -19,6 +28,7 @@ export struct ProgressToSend { export struct MainListModel { checked: bool, header_row: bool, + full_header_row: bool, selected_row: bool, val_str: [string], val_int: [int] diff --git a/krokiet/ui/gui_state.slint b/krokiet/ui/gui_state.slint index 84d38eb..59dc4a4 100644 --- a/krokiet/ui/gui_state.slint +++ b/krokiet/ui/gui_state.slint @@ -1,5 +1,5 @@ import {CurrentTab} from "common.slint"; -import {SelectModel, SelectMode} from "common.slint"; +import {SelectModel, SelectMode, BottomPanelVisibility} from "common.slint"; // State Gui state that shows the current state of the GUI // It extends Settings global state with settings that are not saved to the settings file @@ -17,7 +17,10 @@ export global GuiState { in-out property choosing_include_directories; in-out property visible_tool_settings; - in-out property available_subsettings: active_tab == CurrentTab.SimilarImages; - in-out property active_tab: CurrentTab.EmptyFiles; + in-out property available_subsettings: active_tab == CurrentTab.SimilarImages || active_tab == CurrentTab.DuplicateFiles || active_tab == CurrentTab.SimilarVideos || active_tab == CurrentTab.SimilarMusic || active_tab == CurrentTab.BigFiles || active_tab == CurrentTab.BrokenFiles; + in-out property active_tab: CurrentTab.DuplicateFiles; + in-out property is_tool_tab_active: active_tab != CurrentTab.Settings && active_tab != CurrentTab.About; in-out property <[SelectModel]> select_results_list: [{data: SelectMode.SelectAll, name: "Select All"}, {data: SelectMode.UnselectAll, name: "Deselect All"}, {data: SelectMode.SelectTheSmallestResolution, name: "Select the smallest resolution"}]; + + in-out property bottom_panel_visibility: BottomPanelVisibility.Directories; } diff --git a/krokiet/ui/left_side_panel.slint b/krokiet/ui/left_side_panel.slint index 8d8e349..2587758 100644 --- a/krokiet/ui/left_side_panel.slint +++ b/krokiet/ui/left_side_panel.slint @@ -1,5 +1,5 @@ import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox} from "std-widgets.slint"; -import {CurrentTab} from "common.slint"; +import {CurrentTab, BottomPanelVisibility} from "common.slint"; import {ColorPalette} from "color_palette.slint"; import {GuiState} from "gui_state.slint"; import {Callabler} from "callabler.slint"; @@ -68,40 +68,46 @@ export component LeftSidePanel { callback changed_current_tab(); width: 120px; VerticalLayout { - spacing: 20px; + spacing: 2px; Rectangle { - height: 100px; + visible: GuiState.active_tab != CurrentTab.About; + height: 80px; Image { - width: root.width; + width: parent.height; source: @image-url("../icons/logo.png"); + image-fit: ImageFit.contain; + } + touch_area := TouchArea { + clicked => { + GuiState.active_tab = CurrentTab.About; + Callabler.tab_changed(); + root.changed_current_tab(); + GuiState.bottom_panel_visibility = BottomPanelVisibility.NotVisible; + } } } - VerticalLayout { - // spacing: 3px; - alignment: center; + ListView { out property element-size: 25px; - TabItem { - height: parent.element-size; - scanning: scanning; - text: "Empty Folders"; - curr_tab: CurrentTab.EmptyFolders; - changed_current_tab() => {root.changed_current_tab();} - } + out property <[{name: string, tab: CurrentTab}]> speed_model: [ + {name: "Duplicate Files", tab: CurrentTab.DuplicateFiles}, + {name: "Empty Folders", tab: CurrentTab.EmptyFolders}, + {name: "Big Files", tab: CurrentTab.BigFiles}, + {name: "Empty Files", tab: CurrentTab.EmptyFiles}, + {name: "Temporary Files", tab: CurrentTab.TemporaryFiles}, + {name: "Similar Images", tab: CurrentTab.SimilarImages}, + {name: "Similar Videos", tab: CurrentTab.SimilarVideos}, + {name: "Music Duplicates", tab: CurrentTab.SimilarMusic}, + {name: "Invalid Symlinks", tab: CurrentTab.InvalidSymlinks}, + {name: "Broken Files", tab: CurrentTab.BrokenFiles}, + {name: "Bad Extensions", tab: CurrentTab.BadExtensions} + ]; - TabItem { + for r[idx] in speed_model: TabItem { height: parent.element-size; scanning: scanning; - text: "Empty Files"; - curr_tab: CurrentTab.EmptyFiles; - changed_current_tab() => {root.changed_current_tab();} - } - - TabItem { - height: parent.element-size; - scanning: scanning; - text: "Similar Images"; - curr_tab: CurrentTab.SimilarImages; + text: r.name; + curr_tab: r.tab; changed_current_tab() => {root.changed_current_tab();} } } @@ -109,20 +115,6 @@ export component LeftSidePanel { Rectangle { HorizontalLayout { alignment: start; - Button { - visible: GuiState.active_tab != CurrentTab.Settings && GuiState.available_subsettings; - min-width: 20px; - min-height: 20px; - max-height: self.width; - preferred-height: self.width; - icon: @image-url("../icons/settings.svg"); - clicked => { - GuiState.visible_tool_settings = !GuiState.visible-tool-settings; - } - } - } - HorizontalLayout { - alignment: end; Button { visible: GuiState.active_tab != CurrentTab.Settings; min-width: 20px; @@ -137,6 +129,20 @@ export component LeftSidePanel { } } } + HorizontalLayout { + alignment: end; + Button { + visible: GuiState.available_subsettings; + min-width: 20px; + min-height: 20px; + max-height: self.width; + preferred-height: self.width; + icon: @image-url("../icons/subsettings.svg"); + clicked => { + GuiState.visible_tool_settings = !GuiState.visible-tool-settings; + } + } + } } } } diff --git a/krokiet/ui/main_lists.slint b/krokiet/ui/main_lists.slint index 6ce0d48..cc24d08 100644 --- a/krokiet/ui/main_lists.slint +++ b/krokiet/ui/main_lists.slint @@ -5,56 +5,170 @@ import {CurrentTab, TypeOfOpenedItem} from "common.slint"; import {MainListModel} from "common.slint"; import {SettingsList} from "settings_list.slint"; import {GuiState} from "gui_state.slint"; +import {About} from "about.slint"; export component MainList { + in-out property <[MainListModel]> duplicate_files_model: []; in-out property <[MainListModel]> empty_folder_model: [ - {checked: false, selected_row: false, header_row: true, val_str: ["kropkarz", "/Xd1", "24.10.2023"], val_int: []} , - {checked: false, selected_row: false, header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , - {checked: false, selected_row: false, header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , - {checked: true, selected_row: false, header_row: false, val_str: ["lokkaler", "/Xd1/Vide2", "01.23.1911"], val_int: []} + {checked: false, selected_row: false, header_row: false, full_header_row: false, val_str: ["kropkarz", "/Xd1", "24.10.2023"], val_int: []} , + {checked: false, selected_row: false, header_row: false, full_header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []}, + {checked: true, selected_row: false, header_row: false, full_header_row: false, val_str: ["lokkaler", "/Xd1/Vide2", "01.23.1911"], val_int: []} ]; - in-out property <[MainListModel]> empty_files_model; - in-out property <[MainListModel]> similar_images_model; + in-out property <[MainListModel]> big_files_model: []; + in-out property <[MainListModel]> empty_files_model: [ + {checked: false, selected_row: false, header_row: false, full_header_row: false, val_str: ["kropkarz", "/Xd1", "24.10.2023"], val_int: []} , + {checked: false, selected_row: false, header_row: false, full_header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []}, + {checked: true, selected_row: false, header_row: false, full_header_row: false, val_str: ["lokkaler", "/Xd1/Vide2", "01.23.1911"], val_int: []} + ]; + in-out property <[MainListModel]> temporary_files_model: []; + in-out property <[MainListModel]> similar_images_model: [ + {checked: false, selected_row: false, header_row: true, full_header_row: false, val_str: ["Original", "500KB", "100x100", "kropkarz", "/Xd1", "24.10.2023"], val_int: []}, + {checked: false, selected_row: false, header_row: false, full_header_row: false, val_str: ["Similar", "500KB", "100x100", "witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []}, + {checked: true, selected_row: false, header_row: false, full_header_row: false, val_str: ["Similar", "500KB", "100x100", "lokkaler", "/Xd1/Vide2", "01.23.1911"], val_int: []} + ]; + in-out property <[MainListModel]> similar_videos_model: []; + in-out property <[MainListModel]> similar_music_model: []; + in-out property <[MainListModel]> invalid_symlinks_model: []; + in-out property <[MainListModel]> broken_files_model: []; + in-out property <[MainListModel]> bad_extensions_model: []; + callback changed_current_tab(); callback released_key(string); + + out property path_px: 350px; + out property name_px: 100px; + out property mod_px: 150px; + out property size_px: 75px; + duplicates := SelectableTableView { + visible: GuiState.active_tab == CurrentTab.DuplicateFiles; + min-width: 200px; + height: parent.height; + columns: ["Selection", "Size", "File Name", "Path", "Modification Date"]; + column-sizes: [35px, size_px, name_px, path_px, mod_px]; + values <=> duplicate_files_model; + parentPathIdx: 3; + fileNameIdx: 2; + } + empty_folders := SelectableTableView { visible: GuiState.active_tab == CurrentTab.EmptyFolders; min-width: 200px; height: parent.height; columns: ["Selection", "Folder Name", "Path", "Modification Date"]; - column-sizes: [35px, 100px, 350px, 150px]; + column-sizes: [35px, name_px, path_px, mod_px]; values <=> empty-folder-model; parentPathIdx: 2; fileNameIdx: 1; } + big_files := SelectableTableView { + visible: GuiState.active_tab == CurrentTab.BigFiles; + min-width: 200px; + height: parent.height; + columns: ["Selection", "Size", "File Name", "Path", "Modification Date"]; + column-sizes: [35px, size_px, name_px, path_px, mod_px]; + values <=> big_files_model; + parentPathIdx: 3; + fileNameIdx: 2; + } + empty_files := SelectableTableView { visible: GuiState.active_tab == CurrentTab.EmptyFiles; min-width: 200px; height: parent.height; columns: ["Selection", "File Name", "Path", "Modification Date"]; - column-sizes: [35px, 100px, 350px, 150px]; + column-sizes: [35px, name_px, path_px, mod_px]; values <=> empty-files-model; parentPathIdx: 2; fileNameIdx: 1; } + temporary_files := SelectableTableView { + visible: GuiState.active_tab == CurrentTab.TemporaryFiles; + min-width: 200px; + height: parent.height; + columns: ["Selection", "File Name", "Path", "Modification Date"]; + column-sizes: [35px, name_px, path_px, mod_px]; + values <=> temporary_files_model; + parentPathIdx: 3; + fileNameIdx: 2; + } + similar_images := SelectableTableView { visible: GuiState.active_tab == CurrentTab.SimilarImages; min-width: 200px; height: parent.height; columns: ["Selection", "Similarity", "Size", "Dimensions", "File Name", "Path", "Modification Date"]; - column-sizes: [35px, 80px, 80px, 80px, 100px, 350px, 150px]; + column-sizes: [35px, 80px, 80px, 80px, name_px, path_px, mod_px]; values <=> similar-images-model; parentPathIdx: 5; fileNameIdx: 4; } + similar_videos := SelectableTableView { + visible: GuiState.active_tab == CurrentTab.SimilarVideos; + min-width: 200px; + height: parent.height; + columns: ["Selection", "Size", "File Name", "Path", "Modification Date"]; + column-sizes: [35px, size_px, name_px, path_px, mod_px]; + values <=> similar_videos_model; + parentPathIdx: 3; + fileNameIdx: 2; + } + + similar_music := SelectableTableView { + visible: GuiState.active_tab == CurrentTab.SimilarMusic; + min-width: 200px; + height: parent.height; + columns: ["Selection", "Size", "File Name", "Title","Artist", "Year", "Bitrate", "Length", "Genre", "Path", "Modification Date"]; + column-sizes: [35px, size_px, name_px, 80px, 80px, 80px, 80px, 80px, 80px, path_px, mod_px]; + values <=> similar_music_model; + parentPathIdx: 9; + fileNameIdx: 2; + } + + invalid_symlink := SelectableTableView { + visible: GuiState.active_tab == CurrentTab.InvalidSymlinks; + min-width: 200px; + height: parent.height; + columns: ["Selection", "Symlink Name", "Symlink Folder", "Destination Path", "Modification Date"]; + column-sizes: [35px, name_px, path_px, path_px, mod_px]; + values <=> invalid_symlinks_model; + parentPathIdx: 2; + fileNameIdx: 1; + } + + broken_files := SelectableTableView { + visible: GuiState.active_tab == CurrentTab.BrokenFiles; + min-width: 200px; + height: parent.height; + columns: ["Selection", "File Name", "Path", "Type of Error", "Size", "Modification Date"]; + column-sizes: [35px, name_px, path_px, 200px, size_px, mod_px]; + values <=> broken_files_model; + parentPathIdx: 4; + fileNameIdx: 2; + } + + bad_extensions := SelectableTableView { + visible: GuiState.active_tab == CurrentTab.BadExtensions; + min-width: 200px; + height: parent.height; + columns: ["Selection", "File Name", "Path", "Current Extension", "Proper Extension"]; + column-sizes: [35px, name_px, path_px, 40px, 200px]; + values <=> bad_extensions_model; + parentPathIdx: 4; + fileNameIdx: 2; + } + settings_list := SettingsList { visible: GuiState.active_tab == CurrentTab.Settings; } + about_app := About { + visible: GuiState.active_tab == CurrentTab.About; + } + focus_item := FocusScope { width: 0px; // Hack to not steal first click from other components - https://github.com/slint-ui/slint/issues/3503 // Hack not works https://github.com/slint-ui/slint/issues/3503#issuecomment-1817809834 because disables key-released event diff --git a/krokiet/ui/main_window.slint b/krokiet/ui/main_window.slint index e825c34..d0f28fb 100644 --- a/krokiet/ui/main_window.slint +++ b/krokiet/ui/main_window.slint @@ -28,6 +28,8 @@ export component MainWindow inherits Window { callback show_move_folders_dialog(string); callback folders_move_choose_requested(); + title: "Krokiet - Data Cleaner"; + min-width: 300px; preferred-width: 800px; min-height: 300px; @@ -41,19 +43,17 @@ export component MainWindow inherits Window { all_progress: 20, step_name: "Cache", }; - in-out property <[MainListModel]> empty_folder_model: [ - {checked: false, selected_row: false, header_row: true, val_str: ["kropkarz", "/Xd1", "24.10.2023"], val_int: []} , - {checked: false, selected_row: false, header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , - {checked: false, selected_row: false, header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , - {checked: true, selected_row: false, header_row: false, val_str: ["lokkaler", "/Xd1/Vide2", "01.23.1911"], val_int: []} - ]; - in-out property <[MainListModel]> empty_files_model: [ - {checked: false, selected_row: false, header_row: true, val_str: ["kropkarz", "/Xd1", "24.10.2023"], val_int: []} , - {checked: false, selected_row: false, header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , - {checked: false, selected_row: false, header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , - {checked: true, selected_row: false, header_row: false, val_str: ["lokkaler", "/Xd1/Vide2", "01.23.1911"], val_int: []} - ]; + in-out property <[MainListModel]> duplicate_files_model: []; + in-out property <[MainListModel]> empty_folder_model: []; + in-out property <[MainListModel]> big_files_model: []; + in-out property <[MainListModel]> empty_files_model: []; + in-out property <[MainListModel]> temporary-files_model: []; in-out property <[MainListModel]> similar_images_model: []; + in-out property <[MainListModel]> similar_videos_model: []; + in-out property <[MainListModel]> similar_music_model: []; + in-out property <[MainListModel]> invalid_symlinks_model: []; + in-out property <[MainListModel]> broken_files_model: []; + in-out property <[MainListModel]> bad_extensions_model: []; VerticalBox { HorizontalBox { @@ -78,12 +78,20 @@ export component MainWindow inherits Window { width: preview_or_tool_settings.visible ? parent.width / 2 : parent.width; height: parent.height; horizontal-stretch: 0.5; + duplicate_files_model <=> root.duplicate_files_model; empty_folder_model <=> root.empty_folder_model; + big_files_model <=> root.big_files_model; empty_files_model <=> root.empty_files_model; + temporary-files_model <=> root.temporary-files_model; similar_images_model <=> root.similar_images_model; + similar_videos_model <=> root.similar_videos_model; + similar_music_model <=> root.similar_music_model; + invalid_symlinks_model <=> root.invalid_symlinks_model; + broken_files_model <=> root.broken_files_model; + bad_extensions_model <=> root.bad_extensions_model; } preview_or_tool_settings := Rectangle { - visible: (GuiState.preview_visible || tool_settings.visible) && GuiState.active_tab != CurrentTab.Settings; + visible: (GuiState.preview_visible || tool_settings.visible) && GuiState.is_tool_tab_active; height: parent.height; x: parent.width / 2; width: self.visible ? parent.width / 2 : 0; @@ -134,9 +142,17 @@ export component MainWindow inherits Window { } } - text_summary := LineEdit { - text: text_summary_text; - read-only: true; + HorizontalLayout { + spacing: 5px; + text_summary := LineEdit { + text: text_summary_text; + read-only: true; + } + Text { + text: "Krokiet\n7.0.0"; + vertical-alignment: center; + horizontal-alignment: center; + } } bottom_panel := BottomPanel { diff --git a/krokiet/ui/popup_move_folders.slint b/krokiet/ui/popup_move_folders.slint index 56861b2..e68ae83 100644 --- a/krokiet/ui/popup_move_folders.slint +++ b/krokiet/ui/popup_move_folders.slint @@ -84,10 +84,6 @@ export component PopupMoveFolders inherits Rectangle { } } - init => { - show_popup(); - } - show_popup() => { popup_window.show(); } diff --git a/krokiet/ui/popup_new_directories.slint b/krokiet/ui/popup_new_directories.slint index a920dbf..1de76e5 100644 --- a/krokiet/ui/popup_new_directories.slint +++ b/krokiet/ui/popup_new_directories.slint @@ -72,14 +72,7 @@ export component PopupNewDirectories inherits Rectangle { } } } - - // Button { - // text:"KKK"; - // clicked => { - // show-popup(); - // } - // } - + show_popup() => { popup_window.show(); } diff --git a/krokiet/ui/preview.slint b/krokiet/ui/preview.slint index a7b4ead..5ac00e1 100644 --- a/krokiet/ui/preview.slint +++ b/krokiet/ui/preview.slint @@ -1,3 +1,3 @@ export component Preview inherits Image { - + image-rendering: ImageRendering.smooth; } \ No newline at end of file diff --git a/krokiet/ui/selectable_tree_view.slint b/krokiet/ui/selectable_tree_view.slint index 8a0c730..a526ba4 100644 --- a/krokiet/ui/selectable_tree_view.slint +++ b/krokiet/ui/selectable_tree_view.slint @@ -9,10 +9,10 @@ export component SelectableTableView inherits Rectangle { callback item_opened(string); in property <[string]> columns; in-out property <[MainListModel]> values: [ - {checked: false, selected_row: false, header_row: true, val_str: ["kropkarz", "/Xd1", "24.10.2023"], val_int: []} , - {checked: false, selected_row: false, header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , - {checked: false, selected_row: false, header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , - {checked: true, selected_row: false, header_row: false, val_str: ["lokkaler", "/Xd1/Vide2", "01.23.1911"], val_int: []} + {checked: false, selected_row: false, header_row: true, full_header_row: false, val_str: ["kropkarz", "/Xd1", "24.10.2023"], val_int: []} , + {checked: false, selected_row: false, header_row: false, full_header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , + {checked: false, selected_row: false, header_row: false, full_header_row: false, val_str: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"], val_int: []} , + {checked: true, selected_row: false, header_row: false, full_header_row: false, val_str: ["lokkaler", "/Xd1/Vide2", "01.23.1911"], val_int: []} ]; in-out property <[length]> column_sizes: [30px, 80px, 150px, 160px]; private property column_number: column-sizes.length + 1; @@ -96,11 +96,17 @@ export component SelectableTableView inherits Rectangle { } } double-clicked => { + if (r.header_row && !r.full_header_row) { + return; + } Callabler.item_opened(r.val_str[root.parentPathIdx - 1] + "/" + r.val_str[root.fileNameIdx - 1]) } pointer-event(event) => { // TODO this should be clicked by double-click - https://github.com/slint-ui/slint/issues/4235 if (event.button == PointerEventButton.right && event.kind == PointerEventKind.up) { + if (r.header_row && !r.full_header_row) { + return; + } Callabler.item_opened(r.val_str[root.parentPathIdx - 1]) } else if (event.button == PointerEventButton.left && event.kind == PointerEventKind.up) { clicked_manual(); diff --git a/krokiet/ui/settings.slint b/krokiet/ui/settings.slint index 8634238..d0309ad 100644 --- a/krokiet/ui/settings.slint +++ b/krokiet/ui/settings.slint @@ -40,14 +40,60 @@ export global Settings { // Allowed subsettings - // Duplicate + // Similar Images in-out property <[string]> similar_images_sub_available_hash_size: ["8", "16", "32", "64"]; in-out property similar_images_sub_hash_size_index: 0; + in-out property similar_images_sub_hash_size_value: "8"; in-out property <[string]> similar_images_sub_available_resize_algorithm: ["Lanczos3", "Nearest", "Triangle", "Gaussian", "CatmullRom"]; in-out property similar_images_sub_resize_algorithm_index: 0; + in-out property similar_images_sub_resize_algorithm_value: "Lanczos3"; in-out property <[string]> similar_images_sub_available_hash_type: ["Gradient", "Mean", "VertGradient", "BlockHash", "DoubleGradient"]; - in-out property similar_images_sub_hash_type_index: 0; + in-out property similar_images_sub_hash_alg_index: 0; + in-out property similar_images_sub_hash_alg_value: "Gradient"; in-out property similar_images_sub_max_similarity: 40; in-out property similar_images_sub_current_similarity: 20; in-out property similar_images_sub_ignore_same_size; + + // Duplicates + in-out property <[string]> duplicates_sub_check_method: ["Hash", "Size", "Name", "Size and Name"]; + in-out property duplicates_sub_check_method_index: 0; + in-out property duplicates_sub_check_method_value: "Hash"; + in-out property <[string]> duplicates_sub_available_hash_type: ["Blake3", "CRC32", "XXH3"]; + in-out property duplicates_sub_available_hash_type_index: 0; + in-out property duplicates_sub_available_hash_type_value: "Blake3"; + in-out property duplicates_sub_name_case_sensitive: false; + + + // Big files + in-out property <[string]> biggest_files_sub_method: ["The Biggest", "The Smallest"]; + in-out property biggest_files_sub_method_index: 0; + in-out property biggest_files_sub_method_value: "The Biggest"; + in-out property biggest_files_sub_number_of_files: 50; + + // Similar Videos + in-out property similar_videos_sub_ignore_same_size; + in-out property similar_videos_sub_max_similarity: 20; + in-out property similar_videos_sub_current_similarity: 15; + + // Same Music + in-out property <[string]> similar_music_sub_audio_check_type: ["Tags", "Fingerprint"]; + in-out property similar_music_sub_audio_check_type_index: 0; + in-out property similar_music_sub_audio_check_type_value: "Tags"; + in-out property similar_music_sub_approximate_comparison; + in-out property similar_music_sub_title: true; + in-out property similar_music_sub_artist: true; + in-out property similar_music_sub_year: false; + in-out property similar_music_sub_bitrate: false; + in-out property similar_music_sub_genre: false; + in-out property similar_music_sub_length: false; + in-out property similar_music_sub_minimal_fragment_duration_value: 5.0; + in-out property similar_music_sub_minimal_fragment_duration_max: 180.0; + in-out property similar_music_sub_maximum_difference_value: 3.0; + in-out property similar_music_sub_maximum_difference_max: 10.0; + + // Broken Files + in-out property broken_files_sub_audio: true; + in-out property broken_files_sub_pdf: false; + in-out property broken_files_sub_archive: false; + in-out property broken_files_sub_image: false; } diff --git a/krokiet/ui/settings_list.slint b/krokiet/ui/settings_list.slint index d0352cc..2bd20f8 100644 --- a/krokiet/ui/settings_list.slint +++ b/krokiet/ui/settings_list.slint @@ -9,7 +9,7 @@ global SettingsSize { out property item_height: 30px; } -component TextComponent inherits HorizontalLayout { +export component TextComponent inherits HorizontalLayout { in-out property model; in property name; spacing: 5px; diff --git a/krokiet/ui/tool_settings.slint b/krokiet/ui/tool_settings.slint index 84b28ef..83d3d56 100644 --- a/krokiet/ui/tool_settings.slint +++ b/krokiet/ui/tool_settings.slint @@ -13,11 +13,13 @@ import {ColorPalette} from "color_palette.slint"; import {GuiState} from "gui_state.slint"; import { Preview } from "preview.slint"; import {PopupNewDirectories} from "popup_new_directories.slint"; +import {TextComponent} from "settings_list.slint"; component ComboBoxWrapper inherits HorizontalLayout { in-out property text; in-out property <[string]> model; in-out property current_index; + in-out property current_value; spacing: 5px; Text { text <=> root.text; @@ -26,6 +28,7 @@ component ComboBoxWrapper inherits HorizontalLayout { ComboBox { model: root.model; current_index <=> root.current_index; + current_value <=> root.current_value; } } @@ -62,7 +65,8 @@ component SliderWrapper inherits HorizontalLayout { export component ToolSettings { ScrollView { - if GuiState.active_tab == CurrentTab.SimilarImages: VerticalLayout { + VerticalLayout { + visible: GuiState.active_tab == CurrentTab.SimilarImages; spacing: 5px; padding: 10px; SubsettingsHeader { } @@ -70,16 +74,19 @@ export component ToolSettings { text: "Hash size"; model: Settings.similar_images_sub_available_hash_size; current_index <=> Settings.similar_images_sub_hash_size_index; + current_value <=> Settings.similar_images_sub_hash_size_value; } ComboBoxWrapper { text: "Resize Algorithm"; model: Settings.similar_images_sub_available_resize_algorithm; current_index <=> Settings.similar_images_sub_resize_algorithm_index; + current_value <=> Settings.similar_images_sub_resize_algorithm_value; } ComboBoxWrapper { text: "Hash type"; model: Settings.similar_images_sub_available_hash_type; - current_index <=> Settings.similar_images_sub_hash_type_index; + current_index <=> Settings.similar_images_sub_hash_alg_index; + current_value <=> Settings.similar_images_sub_hash_alg_value; } CheckBoxWrapper { text: "Ignore same size"; @@ -94,5 +101,159 @@ export component ToolSettings { } Rectangle {} } + VerticalLayout { + visible: GuiState.active_tab == CurrentTab.DuplicateFiles; + spacing: 5px; + padding: 10px; + SubsettingsHeader { } + ComboBoxWrapper { + text: "Check method"; + model: Settings.duplicates_sub_check_method; + current_index <=> Settings.duplicates_sub_check_method_index; + current_value <=> Settings.duplicates_sub_check_method_value; + } + ComboBoxWrapper { + text: "Hash type"; + model: Settings.duplicates_sub_available_hash_type; + current_index <=> Settings.duplicates_sub_available_hash_type_index; + current_value <=> Settings.duplicates_sub_available_hash_type_value; + } + CheckBoxWrapper { + text: "Case Sensitive(only name modes)"; + checked <=> Settings.duplicates_sub_name_case_sensitive; + } + Rectangle {} + } + VerticalLayout { + visible: GuiState.active_tab == CurrentTab.BigFiles; + spacing: 5px; + padding: 10px; + SubsettingsHeader { } + ComboBoxWrapper { + text: "Checked files"; + model: Settings.biggest_files_sub_method; + current_index <=> Settings.biggest_files_sub_method_index; + current_value <=> Settings.biggest_files_sub_method_value; + } + TextComponent { + name: "Number of files"; + model <=> Settings.biggest_files_sub_number_of_files; + } + Rectangle {} + } + VerticalLayout { + visible: GuiState.active_tab == CurrentTab.SimilarVideos; + spacing: 5px; + padding: 10px; + SubsettingsHeader { } + SliderWrapper { + text: "Max difference"; + end_text: "(" + round(Settings.similar_videos_sub_current_similarity) + "/" + round(Settings.similar_videos_sub_max_similarity) + ")"; + end_text_size: 40px; + maximum <=> Settings.similar_videos_sub_max_similarity; + value <=> Settings.similar_videos_sub_current_similarity; + } + CheckBoxWrapper { + text: "Ignore same size"; + checked <=> Settings.similar_images_sub_ignore_same_size; + } + Rectangle {} + } + VerticalLayout { + visible: GuiState.active_tab == CurrentTab.SimilarMusic; + spacing: 5px; + padding: 10px; + SubsettingsHeader { } + ComboBoxWrapper { + text: "Audio check type"; + model: Settings.similar_music_sub_audio_check_type; + current_index <=> Settings.similar_music_sub_audio_check_type_index; + current_value <=> Settings.similar_music_sub_audio_check_type_value; + } + // A little bit of a hack + // Mode should be set with a enum, not index, but it's not possible in slint(maybe yet) + if Settings.similar_music_sub_audio_check_type_index == 0: VerticalLayout { + spacing: 5px; + CheckBoxWrapper { + text: "Approximate Tag Comparison"; + checked <=> Settings.similar_music_sub_approximate_comparison; + } + Text { + text: "Compared tags"; + font-size: 12px; + } + CheckBoxWrapper { + text: "Title"; + checked <=> Settings.similar_music_sub_title; + } + CheckBoxWrapper { + text: "Artist"; + checked <=> Settings.similar_music_sub_artist; + } + CheckBoxWrapper { + text: "Bitrate"; + checked <=> Settings.similar_music_sub_bitrate; + } + CheckBoxWrapper { + text: "Genre"; + checked <=> Settings.similar_music_sub_genre; + } + CheckBoxWrapper { + text: "Year"; + checked <=> Settings.similar_music_sub_year; + } + CheckBoxWrapper { + text: "Length"; + checked <=> Settings.similar_music_sub_length; + } + } + if Settings.similar_music_sub_audio_check_type_index == 1: VerticalLayout { + spacing: 5px; + SliderWrapper { + text: "Max difference"; + end_text: "(" + round(Settings.similar_music_sub_maximum_difference_value) + "/" + round(Settings.similar_music_sub_maximum_difference_max) + ")"; + end_text_size: 40px; + maximum <=> Settings.similar_music_sub_maximum_difference_max; + value <=> Settings.similar_music_sub_maximum_difference_value; + } + SliderWrapper { + text: "Minimal fragment duration"; + end_text: round(Settings.similar_music_sub_minimal_fragment_duration_value); + end_text_size: 40px; + maximum <=> Settings.similar_music_sub_minimal_fragment_duration_max; + value <=> Settings.similar_music_sub_minimal_fragment_duration_value; + } + + } + Rectangle {} + } + VerticalLayout { + visible: GuiState.active_tab == CurrentTab.BrokenFiles; + spacing: 5px; + padding: 10px; + SubsettingsHeader { } + Text { + text: "Type of files to check"; + font-size: 12px; + } + CheckBoxWrapper { + text: "Audio"; + checked <=> Settings.broken_files_sub_audio; + } + CheckBoxWrapper { + text: "Pdf"; + checked <=> Settings.broken_files_sub_pdf; + } + CheckBoxWrapper { + text: "Archive"; + checked <=> Settings.broken_files_sub_archive; + } + CheckBoxWrapper { + text: "Image"; + checked <=> Settings.broken_files_sub_image; + } + + Rectangle {} + } } } \ No newline at end of file