1
0
Fork 0
mirror of synced 2024-04-29 10:03:00 +12:00

More work on Krokiet (#1210)

* Disable flaky tests

* More

* More

* About

* TODO

* More

* Heh

* Progress

* A little

* Music

* Models

* Subsettings

* Water

* Header

* Saving

* Poprawa elementów

* Ad

* Names
This commit is contained in:
Rafał Mikrut 2024-02-17 13:53:42 +01:00 committed by GitHub
parent 9599ed5377
commit 0446ff366c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 1979 additions and 441 deletions

View file

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

View file

@ -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<T> {
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<T, Vec<FileEntry>> = BTreeMap::new();

View file

@ -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<FileEntry>,
temporary_files: Vec<TemporaryFileEntry>,
}
impl Temporary {
@ -138,7 +147,7 @@ impl Temporary {
true
}
pub fn get_file_entry(&self, atomic_counter: &Arc<AtomicUsize>, entry_data: &DirEntry, warnings: &mut Vec<String>) -> Option<FileEntry> {
pub fn get_file_entry(&self, atomic_counter: &Arc<AtomicUsize>, entry_data: &DirEntry, warnings: &mut Vec<String>) -> Option<TemporaryFileEntry> {
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, &current_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<FileEntry> {
pub const fn get_temporary_files(&self) -> &Vec<TemporaryFileEntry> {
&self.temporary_files
}

View file

@ -1282,7 +1282,7 @@ fn compute_duplicate_finder(
fn vector_sort_unstable_entry_by_path<T>(vector: &[T]) -> Vec<T>
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<T>(vector: &[T]) -> Vec<T>
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()));

View file

@ -77,8 +77,8 @@ pub fn connect_button_search(gui_data: &GuiData, result_sender: Sender<Message>,
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::<Vec<String>>();
let allowed_extensions = entry_allowed_extensions.text().as_str().to_string();
let excluded_extensions = entry_excluded_extensions.text().as_str().to_string();

View file

@ -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<RefCell<TaskbarProgress>>) {
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,

View file

@ -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);
}

View file

@ -11,6 +11,6 @@ This program is free to use and will always be.
<property name="license-type">mit-x11</property>
<property name="logo-icon-name">help-about-symbolic</property>
<property name="program-name">Czkawka</property>
<property name="version">6.1.0</property>
<property name="version">7.0.0</property>
</object>
</interface>

View file

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

View file

@ -1160,7 +1160,7 @@
<property name="editable">0</property>
<property name="focusable">1</property>
<property name="has-frame">0</property>
<property name="text" translatable="yes">Czkawka 6.1.0</property>
<property name="text" translatable="yes">Czkawka 7.0.0</property>
<property name="xalign">1</property>
</object>
</child>

View file

@ -1 +1,5 @@
<svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><path d="m39.6 27.2c.1-.7.2-1.4.2-2.2s-.1-1.5-.2-2.2l4.5-3.2c.4-.3.6-.9.3-1.4l-4.4-7.4c-.3-.5-.8-.7-1.3-.4l-5 2.3c-1.2-.9-2.4-1.6-3.8-2.2l-.5-5.5c-.1-.5-.5-.9-1-.9h-8.6c-.5 0-1 .4-1 .9l-.5 5.5c-1.4.6-2.7 1.3-3.8 2.2l-5-2.3c-.5-.2-1.1 0-1.3.4l-4.3 7.4c-.3.5-.1 1.1.3 1.4l4.5 3.2c-.1.7-.2 1.4-.2 2.2s.1 1.5.2 2.2l-4.7 3.2c-.4.3-.6.9-.3 1.4l4.3 7.4c.3.5.8.7 1.3.4l5-2.3c1.2.9 2.4 1.6 3.8 2.2l.5 5.5c.1.5.5.9 1 .9h8.6c.5 0 1-.4 1-.9l.5-5.5c1.4-.6 2.7-1.3 3.8-2.2l5 2.3c.5.2 1.1 0 1.3-.4l4.3-7.4c.3-.5.1-1.1-.3-1.4zm-15.6 7.8c-5.5 0-10-4.5-10-10s4.5-10 10-10 10 4.5 10 10-4.5 10-10 10z" fill="#607d8b"/><path d="m24 13c-6.6 0-12 5.4-12 12s5.4 12 12 12 12-5.4 12-12-5.4-12-12-12zm0 17c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5z" fill="#455a64"/></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="m296 186.72a80 80 0 1 0 29.282 109.28 80.24 80.24 0 0 0-29.282-109.28zm109.58 155.64a165.53 165.53 0 0 1-12.59 18.527l23.107 57.358a11.59 11.59 0 0 1-5.1124 14.115l-79.649 45.836a11.64 11.64 0 0 1-14.711-2.8005l-38.08-48.544a176.56 176.56 0 0 1-44.954 0.2228l-37.803 48.357a11.93 11.93 0 0 1-14.898 2.784l-79.778-46.06a12 12 0 0 1-5.203-14.008l22.977-56.917a169.3 169.3 0 0 1-22.274-39.06l-61.08-8.7064a11.64 11.64 0 0 1-9.7856-11.331l-0.13413-91.888a11.59 11.59 0 0 1 9.6676-11.485l61.227-8.6679a174.58 174.58 0 0 1 9.9147-20.453 165.53 165.53 0 0 1 12.59-18.527l-23.107-57.358a11.59 11.59 0 0 1 5.1124-14.115l79.649-45.836a11.64 11.64 0 0 1 14.711 2.8005l38.08 48.544a176.56 176.56 0 0 1 44.954-0.2228l37.803-48.357a11.93 11.93 0 0 1 14.898-2.784l79.778 46.06a12 12 0 0 1 5.203 14.008l-22.977 56.917a169.3 169.3 0 0 1 22.317 39.085l61.037 8.6814a11.64 11.64 0 0 1 9.7856 11.331l0.12913 91.896a11.59 11.59 0 0 1-9.6676 11.485l-61.227 8.6679a174.58 174.58 0 0 1-9.9097 20.444z"/>
</svg>

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="m259.96 178.63c-116.38 0.46054-109.81 154.42-4.8963 157.07 110.82 2.7975 108.63-157.48 4.8963-157.07zm-4.5114 238.17-59.661 58.623c-3.663 4.3684-9.9047 5.5348-14.898 2.784l-79.778-46.06c-4.7847-2.888-6.9422-8.6968-5.203-14.008l22.977-56.917c-9.1283-11.972-16.619-25.108-22.274-39.06l-61.08-8.7064c-5.5804-0.89998-9.7077-5.679-9.7856-11.331l-0.13413-91.888c-0.02721-5.6801 4.0661-10.543 9.6676-11.485l61.227-8.6679c2.8571-7.0258 6.169-13.858 9.9147-20.453 3.7735-6.4529 7.9798-12.643 12.59-18.527l-23.107-57.358c-1.985-5.3221 0.17966-11.298 5.1124-14.115l79.649-45.836c4.933-2.7634 11.138-1.5821 14.711 2.8005l66.966 73.793c-6.8372 161.75-1.2386 214.48-6.8946 306.41z"/>
<rect x="275" y="50" width="200" height="20" stroke-width=".8921"/>
<rect x="275" y="85" width="200" height="20" stroke-width=".8921"/>
<rect x="275" y="120" width="200" height="20" stroke-width=".8921"/>
<rect x="275" y="155" width="200" height="20" stroke-width=".8921"/>
<rect x="335" y="190" width="140" height="20" stroke-width=".74638"/>
<rect x="345" y="225" width="130" height="20" stroke-width=".71923"/>
<rect transform="scale(1,-1)" x="273.16" y="-466.69" width="199.81" height="20" stroke-width=".89168"/>
<rect transform="scale(1,-1)" x="273.16" y="-431.69" width="199.81" height="20" stroke-width=".89168"/>
<rect transform="scale(1,-1)" x="273.16" y="-396.69" width="199.81" height="20" stroke-width=".89168"/>
<rect transform="scale(1,-1)" x="273.16" y="-361.69" width="199.81" height="20" stroke-width=".89168"/>
<rect transform="scale(1,-1)" x="333.1" y="-326.69" width="139.87" height="20" stroke-width=".74603"/>
<rect transform="scale(1,-1)" x="343.09" y="-291.69" width="129.88" height="20" stroke-width=".71889"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -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<MainListMode
CurrentTab::EmptyFolders => 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<MainList
CurrentTab::EmptyFolders => 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] {

View file

@ -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::<Settings>();
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<MainListModel>, active_tab: CurrentTab, remove_to_trash: bool) -> (Vec<String>, Option<ModelRc<MainListModel>>) {
fn handle_delete_items(app: &MainWindow, items: &ModelRc<MainListModel>, active_tab: CurrentTab, remove_to_trash: bool) -> (Vec<String>, Option<ModelRc<MainListModel>>) {
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<MainListModel>, 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<String>, active_tab: CurrentTab, remove_to_trash: bool) -> Vec<String> {
// Iterate over empty folders and not delete them if they are not empty
if active_tab == CurrentTab::EmptyFolders {

View file

@ -37,4 +37,13 @@ pub fn connect_open_items(app: &MainWindow) {
error!("Failed to open cache folder {:?}: {e}", cache_folder);
}
});
app.global::<Callabler>().on_open_link(move |link| {
match open::that(link.as_str()) {
Ok(()) => {}
Err(e) => {
eprintln!("Failed to open link: {e}");
}
};
});
}

View file

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

View file

@ -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<ProgressData>, stop_receiver: Receiver<()>) {
@ -37,155 +49,141 @@ pub fn connect_scan_button(app: &MainWindow, progress_sender: Sender<ProgressDat
let a = app.as_weak();
match active_tab {
CurrentTab::DuplicateFiles => {
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<MainWindow>, progress_sender: Sender<ProgressData>, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) {
// Scan Duplicates
fn scan_duplicates(a: Weak<MainWindow>, progress_sender: Sender<ProgressData>, 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<ImagesEntry>)>, 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::<GuiState>().set_info_text(messages.into());
}
fn write_similar_images_results(app: &MainWindow, vector: Vec<Vec<ImagesEntry>>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_similar_images(fe: &ImagesEntry, hash_size: u8) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<MainWindow>, progress_sender: Sender<ProgressData>, 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::<Vec<_>>();
}
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::<Vec<_>>();
}
_ => 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::<Vec<_>>();
}
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::<Vec<_>>();
}
_ => 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<FileEntry>, messages: String) {
fn write_duplicate_results(app: &MainWindow, vector: Vec<(Option<DuplicateEntry>, Vec<DuplicateEntry>)>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_empty_files(fe: &FileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
fn prepare_data_model_duplicates(fe: &DuplicateEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<FolderEntry>, 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<SharedString>,
(data_model_str, data_model_int)
}
////////////////////////////////////////// Big files
fn scan_big_files(a: Weak<MainWindow>, progress_sender: Sender<ProgressData>, 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<FileEntry>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_big_files(fe: &FileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<MainWindow>, progress_sender: Sender<ProgressData>, 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<FileEntry>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_empty_files(fe: &FileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<MainWindow>, progress_sender: Sender<ProgressData>, 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::<Vec<_>>();
} else {
vector = finder.get_similar_images().iter().cloned().map(|items| (None, items)).collect::<Vec<_>>();
}
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<ImagesEntry>, Vec<ImagesEntry>)>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_similar_images(fe: &ImagesEntry, hash_size: u8) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<MainWindow>, progress_sender: Sender<ProgressData>, 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::<Vec<_>>();
} else {
vector = finder.get_similar_videos().iter().cloned().map(|items| (None, items)).collect::<Vec<_>>();
}
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<VideosEntry>, Vec<VideosEntry>)>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_similar_videos(fe: &VideosEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<MainWindow>, progress_sender: Sender<ProgressData>, 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::<Vec<_>>();
} else {
vector = finder.get_duplicated_music_entries().iter().cloned().map(|items| (None, items)).collect::<Vec<_>>();
}
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<MusicEntry>, Vec<MusicEntry>)>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_similar_music(fe: &MusicEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<MainWindow>, progress_sender: Sender<ProgressData>, 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<SymlinksFileEntry>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_invalid_symlinks(fe: &SymlinksFileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<MainWindow>, progress_sender: Sender<ProgressData>, 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<TemporaryFileEntry>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_temporary_files(fe: &TemporaryFileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<MainWindow>, progress_sender: Sender<ProgressData>, 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<BrokenEntry>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_broken_files(fe: &BrokenEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<MainWindow>, progress_sender: Sender<ProgressData>, 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<BadFileEntry>, 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::<GuiState>().set_info_text(messages.into());
}
fn prepare_data_model_bad_extensions(fe: &BadFileEntry) -> (ModelRc<SharedString>, ModelRc<i32>) {
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<VecModel<MainListModel>>, data_model_str: ModelRc<SharedString>, data_model_int: ModelRc<i32>, header_row: bool) {
fn insert_data_to_model(items: &Rc<VecModel<MainListModel>>, data_model_str: ModelRc<SharedString>, data_model_int: ModelRc<i32>, full_header_row: Option<bool>) {
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),

View file

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

View file

@ -64,10 +64,6 @@ fn main() {
let (progress_sender, progress_receiver): (Sender<ProgressData>, Receiver<ProgressData>) = 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<VecModel<MainListModel>> {
// let header_row_data: Rc<VecModel<MainListModel>> = 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<VecModel<MainListModel>> {
// let non_header_row_data: Rc<VecModel<MainListModel>> = 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
// }

View file

@ -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()),

View file

@ -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::<Settings>();
@ -20,7 +22,19 @@ pub fn set_initial_gui_infos(app: &MainWindow) {
.iter()
.map(|(_settings_key, gui_name, _filter_type)| (*gui_name).into())
.collect::<Vec<_>>();
let available_hash_type: Vec<SharedString> = ALLOWED_HASH_TYPE_VALUES
let available_hash_type: Vec<SharedString> = ALLOWED_IMAGE_HASH_ALG_VALUES
.iter()
.map(|(_settings_key, gui_name, _hash_type)| (*gui_name).into())
.collect::<Vec<_>>();
let available_big_file_search_mode: Vec<SharedString> = ALLOWED_BIG_FILE_SIZE_VALUES
.iter()
.map(|(_settings_key, gui_name, _search_mode)| (*gui_name).into())
.collect::<Vec<_>>();
let available_duplicates_check_method: Vec<SharedString> = ALLOWED_DUPLICATES_CHECK_METHOD_VALUES
.iter()
.map(|(_settings_key, gui_name, _checking_method)| (*gui_name).into())
.collect::<Vec<_>>();
let available_duplicates_hash_type: Vec<SharedString> = ALLOWED_DUPLICATES_HASH_TYPE_VALUES
.iter()
.map(|(_settings_key, gui_name, _hash_type)| (*gui_name).into())
.collect::<Vec<_>>();
@ -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));
}

View file

@ -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<SharedString> = ALLOWED_BIG_FILE_SIZE_VALUES.iter().map(|(_, gui_name, _)| (*gui_name).into()).collect::<Vec<_>>();
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::<GuiState>().set_info_text("".into());
}
@ -468,6 +579,7 @@ pub fn collect_settings(app: &MainWindow) -> SettingsCustom {
.parse::<i32>()
.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<PathBuf> {
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<usize> {
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<usize> {
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<usize> {
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<usize> {
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<usize> {
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<usize> {
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<usize> {
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)
}

67
krokiet/ui/about.slint Normal file
View file

@ -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");
}
}
}
}

View file

@ -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 <BottomPanelVisibility> bottom_panel_visibility: BottomPanelVisibility.Directories;
in-out property <BottomPanelVisibility> bottom_panel_visibility <=> GuiState.bottom_panel_visibility;
in-out property <bool> stop_requested: false;
in-out property <bool> scanning;
in-out property <bool> lists_enabled: GuiState.active_tab != CurrentTab.Settings;
in-out property <bool> lists_enabled: GuiState.is_tool_tab_active;
out property <int> 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";

View file

@ -32,4 +32,6 @@ export global Callabler {
callback open_config_folder();
callback open_cache_folder();
callback open_link(string);
}

View file

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

View file

@ -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 <bool> choosing_include_directories;
in-out property <bool> visible_tool_settings;
in-out property <bool> available_subsettings: active_tab == CurrentTab.SimilarImages;
in-out property <CurrentTab> active_tab: CurrentTab.EmptyFiles;
in-out property <bool> 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 <CurrentTab> active_tab: CurrentTab.DuplicateFiles;
in-out property <bool> 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 <BottomPanelVisibility> bottom_panel_visibility: BottomPanelVisibility.Directories;
}

View file

@ -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 <length> 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;
}
}
}
}
}
}

View file

@ -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 <length> path_px: 350px;
out property <length> name_px: 100px;
out property <length> mod_px: 150px;
out property <length> 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

View file

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

View file

@ -84,10 +84,6 @@ export component PopupMoveFolders inherits Rectangle {
}
}
init => {
show_popup();
}
show_popup() => {
popup_window.show();
}

View file

@ -72,14 +72,7 @@ export component PopupNewDirectories inherits Rectangle {
}
}
}
// Button {
// text:"KKK";
// clicked => {
// show-popup();
// }
// }
show_popup() => {
popup_window.show();
}

View file

@ -1,3 +1,3 @@
export component Preview inherits Image {
image-rendering: ImageRendering.smooth;
}

View file

@ -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 <int> 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();

View file

@ -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 <int> similar_images_sub_hash_size_index: 0;
in-out property <string> 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 <int> similar_images_sub_resize_algorithm_index: 0;
in-out property <string> 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 <int> similar_images_sub_hash_type_index: 0;
in-out property <int> similar_images_sub_hash_alg_index: 0;
in-out property <string> similar_images_sub_hash_alg_value: "Gradient";
in-out property <float> similar_images_sub_max_similarity: 40;
in-out property <float> similar_images_sub_current_similarity: 20;
in-out property <bool> similar_images_sub_ignore_same_size;
// Duplicates
in-out property <[string]> duplicates_sub_check_method: ["Hash", "Size", "Name", "Size and Name"];
in-out property <int> duplicates_sub_check_method_index: 0;
in-out property <string> duplicates_sub_check_method_value: "Hash";
in-out property <[string]> duplicates_sub_available_hash_type: ["Blake3", "CRC32", "XXH3"];
in-out property <int> duplicates_sub_available_hash_type_index: 0;
in-out property <string> duplicates_sub_available_hash_type_value: "Blake3";
in-out property <bool> duplicates_sub_name_case_sensitive: false;
// Big files
in-out property <[string]> biggest_files_sub_method: ["The Biggest", "The Smallest"];
in-out property <int> biggest_files_sub_method_index: 0;
in-out property <string> biggest_files_sub_method_value: "The Biggest";
in-out property <string> biggest_files_sub_number_of_files: 50;
// Similar Videos
in-out property <bool> similar_videos_sub_ignore_same_size;
in-out property <float> similar_videos_sub_max_similarity: 20;
in-out property <float> similar_videos_sub_current_similarity: 15;
// Same Music
in-out property <[string]> similar_music_sub_audio_check_type: ["Tags", "Fingerprint"];
in-out property <int> similar_music_sub_audio_check_type_index: 0;
in-out property <string> similar_music_sub_audio_check_type_value: "Tags";
in-out property <bool> similar_music_sub_approximate_comparison;
in-out property <bool> similar_music_sub_title: true;
in-out property <bool> similar_music_sub_artist: true;
in-out property <bool> similar_music_sub_year: false;
in-out property <bool> similar_music_sub_bitrate: false;
in-out property <bool> similar_music_sub_genre: false;
in-out property <bool> similar_music_sub_length: false;
in-out property <float> similar_music_sub_minimal_fragment_duration_value: 5.0;
in-out property <float> similar_music_sub_minimal_fragment_duration_max: 180.0;
in-out property <float> similar_music_sub_maximum_difference_value: 3.0;
in-out property <float> similar_music_sub_maximum_difference_max: 10.0;
// Broken Files
in-out property <bool> broken_files_sub_audio: true;
in-out property <bool> broken_files_sub_pdf: false;
in-out property <bool> broken_files_sub_archive: false;
in-out property <bool> broken_files_sub_image: false;
}

View file

@ -9,7 +9,7 @@ global SettingsSize {
out property <length> item_height: 30px;
}
component TextComponent inherits HorizontalLayout {
export component TextComponent inherits HorizontalLayout {
in-out property <string> model;
in property <string> name;
spacing: 5px;

View file

@ -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 <string> text;
in-out property <[string]> model;
in-out property <int> current_index;
in-out property <string> 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 {}
}
}
}