1
0
Fork 0
mirror of synced 2024-04-27 17:22:13 +12:00

Excluded extensions and krokiet new features (#1184)

* AVC

* Import split

* Default thread size

* Hen

* Allowed extensions

* Perf

* Connect

* Excluded

* Zmiany

* Optimization

* 4.10

* At once

* Included

* Chang

* VD

* VD

* Hashes

* Wersja

* SD

* Up

* Up

* 2024

* Dup

* Slint files

* Added  select

* Selections

* Fix

* LTO

* Actions

* Added popup delete

* AB

* V4

* Release

* LTO

* Basic moving

* Commonsy

* Moving probably works

* Popup move
This commit is contained in:
Rafał Mikrut 2024-02-14 17:45:25 +01:00 committed by GitHub
parent b63c631e14
commit 378fa1fd6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
104 changed files with 3087 additions and 2520 deletions

View file

@ -12,11 +12,11 @@ jobs:
linux-cli:
strategy:
matrix:
toolchain: [ stable, 1.72.1 ]
toolchain: [ stable, 1.74.0 ]
type: [ release ]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install basic libraries
run: sudo apt update || true; sudo apt install libheif-dev ffmpeg -y
@ -24,12 +24,16 @@ jobs:
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Enable LTO
run: sed -i 's/#lto = "fat"/lto = "fat"/g' Cargo.toml
if: ${{ (matrix.type == 'release') }}
- name: Build Release
run: cargo build --release --bin czkawka_cli
if: ${{ (matrix.type == 'release') }}
- name: Store Linux CLI
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_cli-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/czkawka_cli

View file

@ -16,14 +16,14 @@ jobs:
type: [ release ]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install basic libraries
run: sudo apt update || true; sudo apt install -y ffmpeg
# New versions of nightly rust may call new unimplemented in eyra functions, so use const version
- name: Setup rust version
run: rustup default nightly-2023-12-14
run: rustup default nightly-2024-02-06
- name: Add eyra
run: |
@ -32,12 +32,16 @@ jobs:
echo 'fn main() { println!("cargo:rustc-link-arg=-nostartfiles"); }' > build.rs
cd ..
- name: Enable LTO
run: sed -i 's/#lto = "fat"/lto = "fat"/g' Cargo.toml
if: ${{ (matrix.type == 'release') }}
- name: Build Release
run: cargo build --release --bin czkawka_cli
if: ${{ (matrix.type == 'release') }}
- name: Store Linux CLI
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_cli-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/czkawka_cli

View file

@ -12,21 +12,25 @@ jobs:
linux-krokiet-gui:
strategy:
matrix:
toolchain: [ stable, 1.72.1 ]
toolchain: [ stable, 1.74.0 ]
type: [ release ]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Enable LTO
run: sed -i 's/#lto = "fat"/lto = "fat"/g' Cargo.toml
if: ${{ (matrix.type == 'release') }}
- name: Build Release Krokiet
run: cargo build --release --bin krokiet
if: ${{ (matrix.type == 'release') }}
- name: Store Linux GUI Krokiet
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: krokiet-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/krokiet
@ -35,11 +39,11 @@ jobs:
linux-krokiet-gui-heif:
strategy:
matrix:
toolchain: [ stable, 1.72.1 ]
toolchain: [ stable, 1.74.0 ]
type: [ release ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install basic libraries
run: sudo apt update || true; sudo apt install libheif-dev libraw-dev -y
@ -47,12 +51,16 @@ jobs:
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Enable LTO
run: sed -i 's/#lto = "fat"/lto = "fat"/g' Cargo.toml
if: ${{ (matrix.type == 'release') }}
- name: Build Release Krokiet heif
run: cargo build --release --bin krokiet --features "heif,libraw"
if: ${{ (matrix.type == 'release') }}
- name: Store Linux GUI Krokiet heif libraw
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: krokiet-${{ runner.os }}-${{ matrix.toolchain }}-heif-libraw
path: target/release/krokiet
@ -61,11 +69,11 @@ jobs:
linux-gui:
strategy:
matrix:
toolchain: [ stable, 1.72.1 ]
toolchain: [ stable, 1.74.0 ]
type: [ release ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install basic libraries
run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev libraw-dev -y
@ -73,12 +81,16 @@ jobs:
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Enable LTO
run: sed -i 's/#lto = "fat"/lto = "fat"/g' Cargo.toml
if: ${{ (matrix.type == 'release') }}
- name: Build Release Heif Libraw
run: cargo build --release --bin czkawka_gui --features "heif,libraw"
if: ${{ (matrix.type == 'release') }}
- name: Store Linux GUI Heif Libraw
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_gui-${{ runner.os }}-${{ matrix.toolchain }}-heif-libraw
path: target/release/czkawka_gui
@ -90,7 +102,7 @@ jobs:
# Only store stable toolchain
- name: Store Linux GUI
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_gui-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/czkawka_gui
@ -103,7 +115,7 @@ jobs:
type: [ release ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev librsvg2-dev wget fuse libfuse2 -y
@ -111,6 +123,10 @@ jobs:
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Enable LTO
run: sed -i 's/#lto = "fat"/lto = "fat"/g' Cargo.toml
if: ${{ (matrix.type == 'release') }}
- name: Build Release
run: cargo build --release --bin czkawka_gui
@ -127,7 +143,7 @@ jobs:
./linuxdeploy-x86_64.AppImage --appdir AppDir --plugin gtk --output appimage --icon-file data/icons/com.github.qarmin.czkawka.svg --desktop-file data/com.github.qarmin.czkawka.desktop
- name: Store Linux Appimage GUI
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_gui-appimage-${{ runner.os }}-${{ matrix.toolchain }}
path: Czkawka*.AppImage
@ -144,7 +160,7 @@ jobs:
mv out/Czkawka*.AppImage out/czkawka_gui-minimal.AppImage
- name: Minimal Appimage Upload
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_gui-${{ matrix.toolchain }}_minimal_AppImage
path: out/*.AppImage
@ -156,7 +172,7 @@ jobs:
type: [ debug ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev librsvg2-dev wget fuse libfuse2 -y xvfb

View file

@ -16,7 +16,7 @@ jobs:
type: [ release ]
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install Homebrew
run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
@ -30,26 +30,30 @@ jobs:
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Enable LTO
run: sed -i '' 's/#lto = "fat"/lto = "fat"/g' Cargo.toml
if: ${{ (matrix.type == 'release') }}
- name: Build Release
run: cargo build --release
if: ${{ matrix.type == 'release'}}
- name: Store MacOS CLI
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_cli-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/czkawka_cli
if: ${{ matrix.type == 'release' }}
- name: Store MacOS GUI
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_gui-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/czkawka_gui
if: ${{ matrix.type == 'release' }}
- name: Store MacOS Krokiet
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: krokiet-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/krokiet
@ -60,21 +64,21 @@ jobs:
if: ${{ matrix.type == 'release'}}
- name: Store MacOS CLI Heif
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_cli-${{ runner.os }}-${{ matrix.toolchain }}-heif
path: target/release/czkawka_cli
if: ${{ matrix.type == 'release' }}
- name: Store MacOS GUI Heif
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka_gui-${{ runner.os }}-${{ matrix.toolchain }}-heif
path: target/release/czkawka_gui
if: ${{ matrix.type == 'release' }}
- name: Store MacOS Krokiet Heif
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: krokiet-${{ runner.os }}-${{ matrix.toolchain }}-heif
path: target/release/krokiet

View file

@ -12,7 +12,7 @@ jobs:
quality:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install Gtk 4
run: sudo apt update || true; sudo apt install -y libgtk-4-dev libraw-dev libheif-dev -y

View file

@ -16,7 +16,7 @@ jobs:
use_heif: [ normal ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install dependencies(mostly sd)
run: |
@ -30,11 +30,14 @@ jobs:
run: |
rustup target add x86_64-pc-windows-gnu
- name: Enable LTO
run: sed -i 's/#lto = "fat"/lto = "fat"/g' Cargo.toml
- name: Compile Krokiet
run: cargo build --release --target x86_64-pc-windows-gnu --bin krokiet
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: krokiet-windows-c-on-linux-${{ github.sha }}-${{ matrix.use_heif }}
path: |
@ -50,7 +53,7 @@ jobs:
run: cargo build --release --target x86_64-pc-windows-gnu --bin krokiet
- name: Upload artifacts Console
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: krokiet-windows-c-on-linux-${{ github.sha }}-${{ matrix.use_heif }}-console
path: |
@ -64,7 +67,7 @@ jobs:
use_heif: [ normal ]
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install dependencies(mostly sd)
run: |
@ -75,11 +78,14 @@ jobs:
- name: Setup rust version
run: rustup default stable-x86_64-pc-windows-gnu
- name: Enable LTO
run: sed -i 's/#lto = "fat"/lto = "fat"/g' Cargo.toml
- name: Compile Krokiet
run: cargo build --release --bin krokiet
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: krokiet-windows-c-on-windows-${{ github.sha }}-${{ matrix.use_heif }}
path: |
@ -95,7 +101,7 @@ jobs:
run: cargo build --release --bin krokiet
- name: Upload artifacts Console
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: krokiet-windows-c-on-windows-${{ github.sha }}-${{ matrix.use_heif }}-console
path: |
@ -103,16 +109,16 @@ jobs:
if-no-files-found: error
container:
container_4_10:
strategy:
fail-fast: false
matrix:
use_heif: [ non_heif ]
runs-on: ubuntu-latest
container:
image: ghcr.io/piegamesde/gtk4-cross:gtk-4.6
image: ghcr.io/mglolenstine/gtk4-cross:gtk-4.10
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install additional dependencies
# gio is for the build script
run: |
@ -155,20 +161,79 @@ jobs:
rm gtk4_theme.zip
cd ../..
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka-windows-${{ github.sha }}-${{ matrix.use_heif }}
name: czkawka-windows-${{ github.sha }}-${{ matrix.use_heif }}-4.10
path: |
./package
if-no-files-found: error
container_4_6:
strategy:
fail-fast: false
matrix:
use_heif: [ non_heif ]
runs-on: ubuntu-latest
container:
image: ghcr.io/piegamesde/gtk4-cross:gtk-4.6
steps:
- uses: actions/checkout@v4
- name: Install additional dependencies
# gio is for the build script
run: |
dnf install curl wget2 unzip mingw64-bzip2.noarch mingw64-poppler mingw64-poppler-glib mingw32-python3 rust-gio-devel adwaita-icon-theme -y && dnf clean all -y
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
rustup target add x86_64-pc-windows-gnu
- name: Cross compile for Windows
run: |
source "$HOME/.cargo/env"
#!/bin/bash
set -euo pipefail
export PKG_CONFIG_PATH=/usr/lib64/pkgconfig:/usr/share/pkgconfig:$MINGW_PREFIX/lib/pkgconfig/:/usr/x86_64-w64-mingw32/lib/pkgconfig/
cargo build --target=x86_64-pc-windows-gnu --release --locked
mkdir -p package
cp target/x86_64-pc-windows-gnu/release/czkawka_gui.exe package/
cp target/x86_64-pc-windows-gnu/release/czkawka_cli.exe package/
- name: Package
run: |
#!/bin/bash
set -euo pipefail
cp -t package $(pds -vv -f package/*.exe)
# Add gdbus which is recommended on Windows (why?)
cp $MINGW_PREFIX/bin/gdbus.exe package
# Handle the glib schema compilation as well
glib-compile-schemas $MINGW_PREFIX/share/glib-2.0/schemas/
mkdir -p package/share/glib-2.0/schemas/
cp -T $MINGW_PREFIX/share/glib-2.0/schemas/gschemas.compiled package/share/glib-2.0/schemas/gschemas.compiled
# Pixbuf stuff, in order to get SVGs (scalable icons) to load
mkdir -p package/lib/gdk-pixbuf-2.0
cp -rT $MINGW_PREFIX/lib/gdk-pixbuf-2.0 package/lib/gdk-pixbuf-2.0
cp -f -t package $(pds -vv -f $MINGW_PREFIX/lib/gdk-pixbuf-2.0/2.10.0/loaders/*)
find package -iname "*.dll" -or -iname "*.exe" -type f -exec mingw-strip {} +
cd package/share
wget2 https://github.com/qarmin/czkawka/files/10832192/gtk4_theme.zip
unzip gtk4_theme.zip
rm gtk4_theme.zip
cd ../..
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: czkawka-windows-${{ github.sha }}-${{ matrix.use_heif }}-4.6
path: |
./package
if-no-files-found: error
# Provide option to log things to windows CLI
container_console_window:
container_4_6_console_window:
runs-on: ubuntu-22.04
container:
image: ghcr.io/piegamesde/gtk4-cross:gtk-4.6
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install dependencies(mostly sd)
run: |
dnf install wget -y
@ -219,9 +284,9 @@ jobs:
rm gtk4_theme.zip
cd ../..
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: czkawka-windows-${{ github.sha }}-console
name: czkawka-windows-${{ github.sha }}-console-4.6
path: |
./package
if-no-files-found: error

5
.gitignore vendored
View file

@ -18,4 +18,7 @@ ci_tester/Cargo.lock
krokiet/Cargo.lock
krokiet/target
*.json
*.mm_profdata
*.mm_profdata
perf.data
perf.data.old
krokiet/ui/test.slint

1132
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,7 @@ resolver = "2"
panic = "unwind"
# LTO setting is disabled by default, because release mode is usually needed to develop app and compilation with LTO would take a lot of time
# But it is used to optimize release builds(and probably also in CI, where time is not so important as in local development)
#lto = "fat"
# Optimize all dependencies except application/workspaces, even in debug builds

View file

@ -10,9 +10,9 @@
### CLI
- Providing full static rust binary with [Eyra](https://github.com/sunfishcode/eyra) - [#1102](https://github.com/qarmin/czkawka/pull/1102)
- Fixed duplicated `-c` argument, now saving as compact json is handled via `-C` - [#1153](https://github.com/qarmin/czkawka/pull/1153)
- Added progress bar - [#TODO]()
- Clean and safe cancelling of scan - [#TODO]()
- Unification of CLI arguments - [#TODO]()
- Added scan progress bar - [#1183](https://github.com/qarmin/czkawka/pull/1183)
- Clean and safe cancelling of scan - [#1183](https://github.com/qarmin/czkawka/pull/1183)
- Unification of CLI arguments - [#1183](https://github.com/qarmin/czkawka/pull/1183)
### Krokiet GUI
- Initial release of new gui - [#1102](https://github.com/qarmin/czkawka/pull/1102)
@ -31,6 +31,9 @@
- Unifying code for collecting files to scan - [#1159](https://github.com/qarmin/czkawka/pull/1159)
- Decrease memory usage when collecting files by removing unused fields in custom file entries structs - [#1159](https://github.com/qarmin/czkawka/pull/1159)
- Decrease a little size of cache by few percents and improve loading/saving speed - [#1159](https://github.com/qarmin/czkawka/pull/1159)
- Added ability to remove from scan files with excluded extensions - [#1184](https://github.com/qarmin/czkawka/pull/1102)
- Fixed not showing in similar images results, files with same hashes when using reference folders - [#1184](https://github.com/qarmin/czkawka/pull/1102)
- Optimize release binaries with LTO(~25% smaller, ~5/10% faster) - [#1184](https://github.com/qarmin/czkawka/pull/1102)
## Version 6.1.0 - 15.10.2023r
- BREAKING CHANGE - Changed cache saving method, deduplicated, optimized and simplified procedure(all files needs to be hashed again) - [#1072](https://github.com/qarmin/czkawka/pull/1072), [#1086](https://github.com/qarmin/czkawka/pull/1086)

View file

@ -1,9 +1,10 @@
use log::info;
use std::collections::BTreeSet;
use std::fs;
use std::process::Command;
use std::process::Stdio;
use log::info;
#[derive(Default, Clone, Debug)]
struct CollectedFiles {
files: BTreeSet<String>,

View file

@ -3,14 +3,14 @@ name = "czkawka_cli"
version = "6.1.0"
authors = ["Rafał Mikrut <mikrutrafal@protonmail.com>"]
edition = "2021"
rust-version = "1.72.1"
rust-version = "1.74.0"
description = "CLI frontend of Czkawka"
license = "MIT"
homepage = "https://github.com/qarmin/czkawka"
repository = "https://github.com/qarmin/czkawka"
[dependencies]
clap = { version = "4.4", features = ["derive"] }
clap = { version = "4.5", features = ["derive"] }
# For enum types
image_hasher = "1.2"

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2023 Rafał Mikrut
Copyright (c) 2020-2024 Rafał Mikrut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -25,7 +25,7 @@ cargo run --release --bin czkawka_cli --features "heif,libraw"
```
on linux to build fully static binary with eyra you need to use (this is only for crazy people, so just use command above if you don't know what you are doing)
```shell
rustup default nightly-2023-11-16 # or any newer nightly that works fine with eyra
rustup default nightly-2024-02-06 # or any newer nightly that works fine with eyra
cd czkawka_cli
cargo add eyra --rename=std
echo 'fn main() { println!("cargo:rustc-link-arg=-nostartfiles"); }' > build.rs

View file

@ -428,6 +428,8 @@ pub struct CommonCliItems {
long_help = "List of checked files with provided extension(s). There are also helpful macros which allow to easy use a typical extensions like:\nIMAGE(\"jpg,kra,gif,png,bmp,tiff,hdr,svg\"),\nTEXT(\"txt,doc,docx,odt,rtf\"),\nVIDEO(\"mp4,flv,mkv,webm,vob,ogv,gifv,avi,mov,wmv,mpg,m4v,m4p,mpeg,3gp\") or\nMUSIC(\"mp3,flac,ogg,tta,wma,webm\")\n "
)]
pub allowed_extensions: Vec<String>,
#[clap(short = 'E', long, help = "Excluded file extension(s)", long_help = "List of extensions, that will be removed from search.\n ")]
pub excluded_extensions: Vec<String>,
#[clap(flatten)]
pub file_to_save: FileToSave,
#[clap(flatten)]

View file

@ -10,7 +10,7 @@ use commands::Commands;
use czkawka_core::bad_extensions::BadExtensions;
use czkawka_core::big_file::{BigFile, SearchMode};
use czkawka_core::broken_files::BrokenFiles;
use czkawka_core::common::{print_version_mode, set_number_of_threads, setup_logger};
use czkawka_core::common::{print_version_mode, set_number_of_threads, setup_logger, DEFAULT_THREAD_SIZE};
use czkawka_core::common_dir_traversal::ProgressData;
use czkawka_core::common_tool::{CommonData, DeleteMethod};
#[allow(unused_imports)] // It is used in release for print_results_to_output().
@ -46,7 +46,7 @@ fn main() {
let (progress_sender, progress_receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();
let (stop_sender, stop_receiver): (Sender<()>, Receiver<()>) = bounded(1);
let calculate_thread = thread::spawn(move || match command {
let calculate_thread = thread::Builder::new().stack_size(DEFAULT_THREAD_SIZE).spawn(move || match command {
Commands::Duplicates(duplicates_args) => duplicates(duplicates_args, &stop_receiver, &progress_sender),
Commands::EmptyFolders(empty_folders_args) => empty_folders(empty_folders_args, &stop_receiver, &progress_sender),
Commands::BiggestFiles(biggest_files_args) => biggest_files(biggest_files_args, &stop_receiver, &progress_sender),
@ -70,7 +70,7 @@ fn main() {
connect_progress(&progress_receiver);
calculate_thread.join().unwrap();
calculate_thread.unwrap().join().unwrap();
}
fn duplicates(duplicates: DuplicatesArgs, stop_receiver: &Receiver<()>, progress_sender: &Sender<ProgressData>) {

View file

@ -3,7 +3,7 @@ name = "czkawka_core"
version = "6.1.0"
authors = ["Rafał Mikrut <mikrutrafal@protonmail.com>"]
edition = "2021"
rust-version = "1.72.1"
rust-version = "1.74.0"
description = "Core of Czkawka app"
license = "MIT"
homepage = "https://github.com/qarmin/czkawka"
@ -34,15 +34,15 @@ audio_checker = "0.1"
pdf = "0.9"
# Needed by audio similarity feature
rusty-chromaprint = "0.1"
rusty-chromaprint = "0.2"
symphonia = { version = "0.5", features = ["all"] }
# Hashes for duplicate files
blake3 = "1.5"
crc32fast = "1.3"
crc32fast = "1.4"
xxhash-rust = { version = "0.8", features = ["xxh3"] }
tempfile = "3.9"
tempfile = "3.10"
# Video Duplicates
vid_dup_finder_lib = "0.1"
@ -55,7 +55,7 @@ serde_json = "1.0"
# Language
i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.7"
i18n-embed-fl = "0.8"
rust-embed = { version = "8.2", features = ["debug-embed"] }
once_cell = "1.19"
@ -75,6 +75,7 @@ anyhow = { version = "1.0" }
state = "0.6"
trash = "3.3"
os_info = { version = "3", default-features = false }
log = "0.4.20"

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2023 Rafał Mikrut
Copyright (c) 2020-2024 Rafał Mikrut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -195,7 +195,7 @@ impl BadExtensions {
#[fun_time(message = "find_bad_extensions_files", level = "info")]
pub fn find_bad_extensions_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
return;

View file

@ -43,7 +43,7 @@ impl BigFile {
#[fun_time(message = "find_big_files", level = "info")]
pub fn find_big_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
if !self.look_for_big_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
return;
@ -60,6 +60,7 @@ impl BigFile {
.progress_sender(progress_sender)
.common_data(&self.common_data)
.minimal_file_size(1)
.maximal_file_size(u64::MAX)
.max_stage(0)
.build()
.run();

View file

@ -1,7 +1,6 @@
use std::collections::{BTreeMap, HashSet};
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use std::{fs, mem, panic};
@ -107,7 +106,7 @@ impl BrokenFiles {
#[fun_time(message = "find_broken_files", level = "info")]
pub fn find_broken_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
return;
@ -140,8 +139,8 @@ impl BrokenFiles {
}
}
self.common_data.allowed_extensions.set_and_validate_extensions(&extensions);
if !self.common_data.allowed_extensions.set_any_extensions() {
self.common_data.extensions.set_and_validate_allowed_extensions(&extensions);
if !self.common_data.extensions.set_any_extensions() {
return true;
}

View file

@ -1,4 +1,6 @@
#![allow(unused_imports)]
use std::{fs, thread};
// I don't wanna fight with unused imports in this file, so simply ignore it to avoid too much complexity
use std::cmp::Ordering;
use std::ffi::OsString;
@ -8,7 +10,6 @@ use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::sync::{atomic, Arc};
use std::thread::{sleep, JoinHandle};
use std::time::{Duration, Instant, SystemTime};
use std::{fs, thread};
#[cfg(feature = "heif")]
use anyhow::Result;
@ -38,20 +39,16 @@ use crate::duplicate::make_hard_link;
use crate::CZKAWKA_VERSION;
static NUMBER_OF_THREADS: state::InitCell<usize> = state::InitCell::new();
static ALL_AVAILABLE_THREADS: state::InitCell<usize> = state::InitCell::new();
pub const DEFAULT_THREAD_SIZE: usize = 8 * 1024 * 1024; // 8 MB
pub const DEFAULT_WORKER_THREAD_SIZE: usize = 4 * 1024 * 1024; // 4 MB
#[cfg(not(target_family = "windows"))]
pub const CHARACTER: char = '/';
#[cfg(target_family = "windows")]
pub const CHARACTER: char = '\\';
pub fn get_number_of_threads() -> usize {
let data = NUMBER_OF_THREADS.get();
if *data >= 1 {
*data
} else {
get_default_number_of_threads()
get_all_available_threads()
}
}
@ -70,15 +67,19 @@ pub fn setup_logger(disabled_printing: bool) {
handsome_logger::TermLogger::init(config, TerminalMode::Mixed, ColorChoice::Always).unwrap();
}
pub fn get_available_threads() -> usize {
thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(1)
pub fn get_all_available_threads() -> usize {
*ALL_AVAILABLE_THREADS.get_or_init(|| {
let available_threads = thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(1);
ALL_AVAILABLE_THREADS.set(available_threads);
available_threads
})
}
pub fn print_version_mode() {
let rust_version = env!("RUST_VERSION_INTERNAL");
let debug_release = if cfg!(debug_assertions) { "debug" } else { "release" };
let processors = get_available_threads();
let processors = get_all_available_threads();
let info = os_info::get();
info!(
@ -98,11 +99,7 @@ pub fn print_version_mode() {
}
pub fn set_default_number_of_threads() {
set_number_of_threads(get_default_number_of_threads());
}
pub fn get_default_number_of_threads() -> usize {
thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(1)
set_number_of_threads(get_all_available_threads());
}
pub fn set_number_of_threads(thread_number: usize) {
@ -149,22 +146,21 @@ pub const VIDEO_FILES_EXTENSIONS: &[&str] = &[
pub const LOOP_DURATION: u32 = 20; //ms
pub const SEND_PROGRESS_DATA_TIME_BETWEEN: u32 = 200; //ms
pub fn remove_folder_if_contains_only_empty_folders(path: impl AsRef<Path>) -> bool {
pub fn remove_folder_if_contains_only_empty_folders(path: impl AsRef<Path>, remove_to_trash: bool) -> Result<(), String> {
let path = path.as_ref();
if !path.is_dir() {
error!("Trying to remove folder which is not a directory");
return false;
return Err(format!("Trying to remove folder {path:?} which is not a directory",));
}
let mut entries_to_check = Vec::new();
let Ok(initial_entry) = path.read_dir() else {
return false;
return Err(format!("Cannot read directory {path:?}",));
};
for entry in initial_entry {
if let Ok(entry) = entry {
entries_to_check.push(entry);
} else {
return false;
return Err(format!("Cannot read entry from directory {path:?}"));
}
}
loop {
@ -172,25 +168,29 @@ pub fn remove_folder_if_contains_only_empty_folders(path: impl AsRef<Path>) -> b
break;
};
let Some(file_type) = entry.file_type().ok() else {
return false;
return Err(format!("Folder contains file with unknown type {:?} inside {path:?}", entry.path()));
};
if !file_type.is_dir() {
return false;
return Err(format!("Folder contains file {:?} inside {path:?}", entry.path(),));
}
let Ok(internal_read_dir) = entry.path().read_dir() else {
return false;
return Err(format!("Cannot read directory {:?} inside {path:?}", entry.path()));
};
for internal_elements in internal_read_dir {
if let Ok(internal_element) = internal_elements {
entries_to_check.push(internal_element);
} else {
return false;
return Err(format!("Cannot read entry from directory {:?} inside {path:?}", entry.path()));
}
}
}
fs::remove_dir_all(path).is_ok()
if remove_to_trash {
trash::delete(path).map_err(|e| format!("Cannot move folder {path:?} to trash, reason {e}"))
} else {
fs::remove_dir_all(path).map_err(|e| format!("Cannot remove directory {path:?}, reason {e}"))
}
}
pub fn open_cache_folder(cache_file_name: &str, save_to_cache: bool, use_json: bool, warnings: &mut Vec<String>) -> Option<((Option<File>, PathBuf), (Option<File>, PathBuf))> {
@ -273,7 +273,6 @@ pub fn get_dynamic_image_from_raw_image(path: impl AsRef<Path>) -> Option<Dynami
let width = processed.width();
let height = processed.height();
dbg!(width, height);
let data = processed.to_vec();
@ -612,10 +611,11 @@ mod test {
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use tempfile::tempdir;
use crate::common::{normalize_windows_path, regex_check, remove_folder_if_contains_only_empty_folders};
use crate::common_items::{new_excluded_item, ExcludedItems};
use crate::common_items::new_excluded_item;
#[test]
fn test_remove_folder_if_contains_only_empty_folders() {
@ -624,20 +624,20 @@ mod test {
fs::create_dir(&sub_dir).unwrap();
// Test with empty directory
assert!(remove_folder_if_contains_only_empty_folders(&sub_dir));
assert!(remove_folder_if_contains_only_empty_folders(&sub_dir, false).is_ok());
assert!(!Path::new(&sub_dir).exists());
// Test with directory containing an empty directory
fs::create_dir(&sub_dir).unwrap();
fs::create_dir(sub_dir.join("empty_sub_dir")).unwrap();
assert!(remove_folder_if_contains_only_empty_folders(&sub_dir));
assert!(remove_folder_if_contains_only_empty_folders(&sub_dir, false).is_ok());
assert!(!Path::new(&sub_dir).exists());
// Test with directory containing a file
fs::create_dir(&sub_dir).unwrap();
let mut file = fs::File::create(sub_dir.join("file.txt")).unwrap();
writeln!(file, "Hello, world!").unwrap();
assert!(!remove_folder_if_contains_only_empty_folders(&sub_dir));
assert!(remove_folder_if_contains_only_empty_folders(&sub_dir, false).is_err());
assert!(Path::new(&sub_dir).exists());
}

View file

@ -1,16 +1,18 @@
use crate::common;
use crate::common_messages::Messages;
use crate::common_traits::ResultEntry;
use crate::duplicate::HashType;
use crate::similar_images::{convert_algorithm_to_string, convert_filters_to_string};
use std::collections::BTreeMap;
use std::io::{BufReader, BufWriter};
use fun_time::fun_time;
use image::imageops::FilterType;
use image_hasher::HashAlg;
use log::debug;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::io::{BufReader, BufWriter};
use crate::common;
use crate::common_messages::Messages;
use crate::common_traits::ResultEntry;
use crate::duplicate::HashType;
use crate::similar_images::{convert_algorithm_to_string, convert_filters_to_string};
const CACHE_VERSION: &str = "70";

View file

@ -20,7 +20,6 @@ use crate::common_items::ExcludedItems;
use crate::common_tool::CommonToolData;
use crate::common_traits::ResultEntry;
use crate::flc;
use crate::localizer_core::generate_translation_hashmap;
#[derive(Debug)]
pub struct ProgressData {
@ -115,7 +114,7 @@ pub struct DirTraversalBuilder<'a, 'b, F> {
recursive_search: bool,
directories: Option<Directories>,
excluded_items: Option<ExcludedItems>,
allowed_extensions: Option<Extensions>,
extensions: Option<Extensions>,
tool_type: ToolType,
}
@ -127,7 +126,7 @@ pub struct DirTraversal<'a, 'b, F> {
recursive_search: bool,
directories: Directories,
excluded_items: ExcludedItems,
allowed_extensions: Extensions,
extensions: Extensions,
minimal_file_size: u64,
maximal_file_size: u64,
checking_method: CheckingMethod,
@ -156,7 +155,7 @@ impl<'a, 'b> DirTraversalBuilder<'a, 'b, ()> {
collect: Collect::Files,
recursive_search: false,
directories: None,
allowed_extensions: None,
extensions: None,
excluded_items: None,
tool_type: ToolType::None,
}
@ -171,7 +170,7 @@ impl<'a, 'b, F> DirTraversalBuilder<'a, 'b, F> {
pub fn common_data(mut self, common_tool_data: &CommonToolData) -> Self {
self.root_dirs = common_tool_data.directories.included_directories.clone();
self.allowed_extensions = Some(common_tool_data.allowed_extensions.clone());
self.extensions = Some(common_tool_data.extensions.clone());
self.excluded_items = Some(common_tool_data.excluded_items.clone());
self.recursive_search = common_tool_data.recursive_search;
self.minimal_file_size = Some(common_tool_data.minimal_file_size);
@ -221,8 +220,8 @@ impl<'a, 'b, F> DirTraversalBuilder<'a, 'b, F> {
self
}
pub fn allowed_extensions(mut self, allowed_extensions: Extensions) -> Self {
self.allowed_extensions = Some(allowed_extensions);
pub fn extensions(mut self, extensions: Extensions) -> Self {
self.extensions = Some(extensions);
self
}
@ -260,7 +259,7 @@ impl<'a, 'b, F> DirTraversalBuilder<'a, 'b, F> {
stop_receiver: self.stop_receiver,
progress_sender: self.progress_sender,
directories: self.directories,
allowed_extensions: self.allowed_extensions,
extensions: self.extensions,
excluded_items: self.excluded_items,
recursive_search: self.recursive_search,
maximal_file_size: self.maximal_file_size,
@ -285,7 +284,7 @@ impl<'a, 'b, F> DirTraversalBuilder<'a, 'b, F> {
collect: self.collect,
directories: self.directories.expect("could not build"),
excluded_items: self.excluded_items.expect("could not build"),
allowed_extensions: self.allowed_extensions.unwrap_or_default(),
extensions: self.extensions.unwrap_or_default(),
recursive_search: self.recursive_search,
tool_type: self.tool_type,
}
@ -334,7 +333,7 @@ where
collect,
directories,
excluded_items,
allowed_extensions,
extensions,
recursive_search,
minimal_file_size,
maximal_file_size,
@ -377,7 +376,7 @@ where
entry_data,
&mut warnings,
&mut fe_result,
&allowed_extensions,
&extensions,
&directories,
&excluded_items,
minimal_file_size,
@ -389,7 +388,7 @@ where
}
(EntryType::Symlink, Collect::InvalidSymlinks) => {
counter += 1;
process_symlink_in_symlink_mode(entry_data, &mut warnings, &mut fe_result, &allowed_extensions, &directories, &excluded_items);
process_symlink_in_symlink_mode(entry_data, &mut warnings, &mut fe_result, &extensions, &directories, &excluded_items);
}
(EntryType::Symlink, Collect::Files) | (EntryType::Other, _) => {
// nothing to do
@ -435,13 +434,13 @@ fn process_file_in_file_mode(
entry_data: &DirEntry,
warnings: &mut Vec<String>,
fe_result: &mut Vec<FileEntry>,
allowed_extensions: &Extensions,
extensions: &Extensions,
directories: &Directories,
excluded_items: &ExcludedItems,
minimal_file_size: u64,
maximal_file_size: u64,
) {
if !allowed_extensions.check_if_entry_ends_with_extension(entry_data) {
if !extensions.check_if_entry_have_valid_extension(entry_data) {
return;
}
@ -512,11 +511,11 @@ fn process_symlink_in_symlink_mode(
entry_data: &DirEntry,
warnings: &mut Vec<String>,
fe_result: &mut Vec<FileEntry>,
allowed_extensions: &Extensions,
extensions: &Extensions,
directories: &Directories,
excluded_items: &ExcludedItems,
) {
if !allowed_extensions.check_if_entry_ends_with_extension(entry_data) {
if !extensions.check_if_entry_have_valid_extension(entry_data) {
return;
}
@ -560,10 +559,7 @@ pub fn common_read_dir(current_folder: &Path, warnings: &mut Vec<String>) -> Opt
Some(r)
}
Err(e) => {
warnings.push(flc!(
"core_cannot_open_dir",
generate_translation_hashmap(vec![("dir", current_folder.to_string_lossy().to_string()), ("reason", e.to_string())])
));
warnings.push(flc!("core_cannot_open_dir", dir = current_folder.to_string_lossy().to_string(), reason = e.to_string()));
None
}
}
@ -574,7 +570,8 @@ pub fn common_get_entry_data<'a>(entry: &'a Result<DirEntry, std::io::Error>, wa
Err(e) => {
warnings.push(flc!(
"core_cannot_read_entry_dir",
generate_translation_hashmap(vec![("dir", current_folder.to_string_lossy().to_string()), ("reason", e.to_string())])
dir = current_folder.to_string_lossy().to_string(),
reason = e.to_string()
));
return None;
}
@ -587,7 +584,8 @@ pub fn common_get_metadata_dir(entry_data: &DirEntry, warnings: &mut Vec<String>
Err(e) => {
warnings.push(flc!(
"core_cannot_read_metadata_dir",
generate_translation_hashmap(vec![("dir", current_folder.to_string_lossy().to_string()), ("reason", e.to_string())])
dir = current_folder.to_string_lossy().to_string(),
reason = e.to_string()
));
return None;
}
@ -601,7 +599,8 @@ pub fn common_get_entry_data_metadata<'a>(entry: &'a Result<DirEntry, std::io::E
Err(e) => {
warnings.push(flc!(
"core_cannot_read_entry_dir",
generate_translation_hashmap(vec![("dir", current_folder.to_string_lossy().to_string()), ("reason", e.to_string())])
dir = current_folder.to_string_lossy().to_string(),
reason = e.to_string()
));
return None;
}
@ -611,7 +610,8 @@ pub fn common_get_entry_data_metadata<'a>(entry: &'a Result<DirEntry, std::io::E
Err(e) => {
warnings.push(flc!(
"core_cannot_read_metadata_dir",
generate_translation_hashmap(vec![("dir", current_folder.to_string_lossy().to_string()), ("reason", e.to_string())])
dir = current_folder.to_string_lossy().to_string(),
reason = e.to_string()
));
return None;
}
@ -624,21 +624,27 @@ pub fn get_modified_time(metadata: &Metadata, warnings: &mut Vec<String>, curren
Ok(t) => match t.duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs(),
Err(_inspected) => {
let translation_hashmap = generate_translation_hashmap(vec![("name", current_file_name.to_string_lossy().to_string())]);
if is_folder {
warnings.push(flc!("core_folder_modified_before_epoch", translation_hashmap));
warnings.push(flc!("core_folder_modified_before_epoch", name = current_file_name.to_string_lossy().to_string()));
} else {
warnings.push(flc!("core_file_modified_before_epoch", translation_hashmap));
warnings.push(flc!("core_file_modified_before_epoch", name = current_file_name.to_string_lossy().to_string()));
}
0
}
},
Err(e) => {
let translation_hashmap = generate_translation_hashmap(vec![("name", current_file_name.to_string_lossy().to_string()), ("reason", e.to_string())]);
if is_folder {
warnings.push(flc!("core_folder_no_modification_date", translation_hashmap));
warnings.push(flc!(
"core_folder_no_modification_date",
name = current_file_name.to_string_lossy().to_string(),
reason = e.to_string()
));
} else {
warnings.push(flc!("core_file_no_modification_date", translation_hashmap));
warnings.push(flc!(
"core_file_no_modification_date",
name = current_file_name.to_string_lossy().to_string(),
reason = e.to_string()
));
}
0
}

View file

@ -1,11 +1,10 @@
use crate::common::normalize_windows_path;
use std::path::{Path, PathBuf};
#[cfg(target_family = "unix")]
use std::{fs, os::unix::fs::MetadataExt};
use crate::common::normalize_windows_path;
use crate::common_messages::Messages;
use crate::flc;
use crate::localizer_core::generate_translation_hashmap;
#[derive(Debug, Clone, Default)]
pub struct Directories {
@ -105,19 +104,15 @@ impl Directories {
let mut directory = directory.to_path_buf();
if !directory.exists() {
if !is_excluded {
messages.warnings.push(flc!(
"core_directory_must_exists",
generate_translation_hashmap(vec![("path", directory.to_string_lossy().to_string())])
));
messages.warnings.push(flc!("core_directory_must_exists", path = directory.to_string_lossy().to_string()));
}
return (None, messages);
}
if !directory.is_dir() {
messages.warnings.push(flc!(
"core_directory_must_be_directory",
generate_translation_hashmap(vec![("path", directory.to_string_lossy().to_string())])
));
messages
.warnings
.push(flc!("core_directory_must_be_directory", path = directory.to_string_lossy().to_string()));
return (None, messages);
}
@ -290,10 +285,7 @@ impl Directories {
for d in &self.included_directories {
match fs::metadata(d) {
Ok(m) => self.included_dev_ids.push(m.dev()),
Err(_) => messages.errors.push(flc!(
"core_directory_unable_to_get_device_id",
generate_translation_hashmap(vec![("path", d.to_string_lossy().to_string())])
)),
Err(_) => messages.errors.push(flc!("core_directory_unable_to_get_device_id", path = d.to_string_lossy().to_string())),
}
}
}
@ -322,10 +314,7 @@ impl Directories {
let path = path.as_ref();
match fs::metadata(path) {
Ok(m) => Ok(!self.included_dev_ids.iter().any(|&id| id == m.dev())),
Err(_) => Err(flc!(
"core_directory_unable_to_get_device_id",
generate_translation_hashmap(vec![("path", path.to_string_lossy().to_string())])
)),
Err(_) => Err(flc!("core_directory_unable_to_get_device_id", path = path.to_string_lossy().to_string())),
}
}
}

View file

@ -1,37 +1,39 @@
use crate::common_messages::Messages;
use std::collections::HashSet;
use std::fs::DirEntry;
use crate::common_messages::Messages;
#[derive(Debug, Clone, Default)]
pub struct Extensions {
file_extensions_hashset: HashSet<String>,
allowed_extensions_hashset: HashSet<String>,
excluded_extensions_hashset: HashSet<String>,
}
impl Extensions {
pub fn new() -> Self {
Default::default()
}
/// List of allowed extensions, only files with this extensions will be checking if are duplicates
/// After, extensions cannot contains any dot, commas etc.
pub fn set_allowed_extensions(&mut self, mut allowed_extensions: String) -> Messages {
pub fn filter_extensions(mut file_extensions: String) -> (HashSet<String>, Messages) {
let mut messages = Messages::new();
let mut extensions_hashset = HashSet::new();
if allowed_extensions.trim().is_empty() {
return messages;
if file_extensions.trim().is_empty() {
return (Default::default(), messages);
}
allowed_extensions = allowed_extensions.replace("IMAGE", "jpg,kra,gif,png,bmp,tiff,hdr,svg");
allowed_extensions = allowed_extensions.replace("VIDEO", "mp4,flv,mkv,webm,vob,ogv,gifv,avi,mov,wmv,mpg,m4v,m4p,mpeg,3gp");
allowed_extensions = allowed_extensions.replace("MUSIC", "mp3,flac,ogg,tta,wma,webm");
allowed_extensions = allowed_extensions.replace("TEXT", "txt,doc,docx,odt,rtf");
file_extensions = file_extensions.replace("IMAGE", "jpg,kra,gif,png,bmp,tiff,hdr,svg");
file_extensions = file_extensions.replace("VIDEO", "mp4,flv,mkv,webm,vob,ogv,gifv,avi,mov,wmv,mpg,m4v,m4p,mpeg,3gp");
file_extensions = file_extensions.replace("MUSIC", "mp3,flac,ogg,tta,wma,webm");
file_extensions = file_extensions.replace("TEXT", "txt,doc,docx,odt,rtf");
let extensions: Vec<String> = allowed_extensions.split(',').map(str::trim).map(String::from).collect();
let extensions: Vec<String> = file_extensions.split(',').map(str::trim).map(String::from).collect();
for mut extension in extensions {
if extension.is_empty() || extension.replace(['.', ' '], "").trim().is_empty() {
continue;
}
if extension.starts_with('.') {
extension = extension[1..].to_string();
extension = extension.chars().skip(1).collect::<String>();
}
if extension.contains('.') {
@ -44,49 +46,61 @@ impl Extensions {
continue;
}
self.file_extensions_hashset.insert(extension);
extensions_hashset.insert(extension);
}
(extensions_hashset, messages)
}
if self.file_extensions_hashset.is_empty() {
messages
.messages
.push("No valid extensions were provided, so allowing all extensions by default.".to_string());
}
/// List of allowed extensions, only files with this extensions will be checking if are duplicates
/// After, extensions cannot contain any dot, commas etc.
pub fn set_allowed_extensions(&mut self, allowed_extensions: String) -> Messages {
let (extensions, messages) = Self::filter_extensions(allowed_extensions);
self.allowed_extensions_hashset = extensions;
messages
}
pub fn matches_filename(&self, file_name: &str) -> bool {
// assert_eq!(file_name, file_name.to_lowercase());
if !self.file_extensions_hashset.is_empty() && !self.file_extensions_hashset.iter().any(|e| file_name.ends_with(e)) {
return false;
}
true
pub fn set_excluded_extensions(&mut self, excluded_extensions: String) -> Messages {
let (extensions, messages) = Self::filter_extensions(excluded_extensions);
self.excluded_extensions_hashset = extensions;
messages
}
pub fn check_if_entry_ends_with_extension(&self, entry_data: &DirEntry) -> bool {
if self.file_extensions_hashset.is_empty() {
pub fn check_if_entry_have_valid_extension(&self, entry_data: &DirEntry) -> bool {
if self.allowed_extensions_hashset.is_empty() && self.excluded_extensions_hashset.is_empty() {
return true;
}
// Using entry_data.path().extension() is a lot of slower, even 5 times
let file_name = entry_data.file_name();
let Some(file_name_str) = file_name.to_str() else { return false };
let Some(extension_idx) = file_name_str.rfind('.') else { return false };
let extension = &file_name_str[extension_idx + 1..];
if extension.chars().all(|c| c.is_ascii_lowercase()) {
self.file_extensions_hashset.contains(extension)
if !self.allowed_extensions_hashset.is_empty() {
if extension.chars().all(|c| c.is_ascii_lowercase()) {
self.allowed_extensions_hashset.contains(extension)
} else {
self.allowed_extensions_hashset.contains(&extension.to_lowercase())
}
} else {
self.file_extensions_hashset.contains(&extension.to_lowercase())
if extension.chars().all(|c| c.is_ascii_lowercase()) {
!self.excluded_extensions_hashset.contains(extension)
} else {
!self.excluded_extensions_hashset.contains(&extension.to_lowercase())
}
}
}
pub fn set_any_extensions(&self) -> bool {
!self.file_extensions_hashset.is_empty()
!self.allowed_extensions_hashset.is_empty()
}
fn extend_allowed_extensions(&mut self, file_extensions: &[&str]) {
for extension in file_extensions {
let extension_without_dot = extension.trim_start_matches('.');
self.file_extensions_hashset.insert(extension_without_dot.to_string());
self.allowed_extensions_hashset.insert(extension_without_dot.to_string());
}
}
@ -100,8 +114,8 @@ impl Extensions {
}
}
pub fn set_and_validate_extensions(&mut self, file_extensions: &[&str]) {
if self.file_extensions_hashset.is_empty() {
pub fn set_and_validate_allowed_extensions(&mut self, file_extensions: &[&str]) {
if self.allowed_extensions_hashset.is_empty() {
self.extend_allowed_extensions(file_extensions);
} else {
self.union_allowed_extensions(file_extensions);

View file

@ -1,8 +1,8 @@
use std::path::Path;
#[cfg(not(target_family = "unix"))]
use crate::common::normalize_windows_path;
use crate::common::regex_check;
use std::path::Path;
use crate::common_messages::Messages;
#[cfg(target_family = "unix")]

View file

@ -9,6 +9,15 @@ impl Messages {
pub fn new() -> Self {
Default::default()
}
pub fn new_from_errors(errors: Vec<String>) -> Self {
Messages { errors, ..Default::default() }
}
pub fn new_from_warnings(warnings: Vec<String>) -> Self {
Messages { warnings, ..Default::default() }
}
pub fn new_from_messages(messages: Vec<String>) -> Self {
Messages { messages, ..Default::default() }
}
pub fn print_messages(&self) {
println!("{}", self.create_messages_text());
}

View file

@ -11,7 +11,7 @@ pub struct CommonToolData {
pub(crate) tool_type: ToolType,
pub(crate) text_messages: Messages,
pub(crate) directories: Directories,
pub(crate) allowed_extensions: Extensions,
pub(crate) extensions: Extensions,
pub(crate) excluded_items: ExcludedItems,
pub(crate) recursive_search: bool,
pub(crate) delete_method: DeleteMethod,
@ -43,7 +43,7 @@ impl CommonToolData {
tool_type,
text_messages: Messages::new(),
directories: Directories::new(),
allowed_extensions: Extensions::new(),
extensions: Extensions::new(),
excluded_items: ExcludedItems::new(),
recursive_search: true,
delete_method: DeleteMethod::None,
@ -168,7 +168,11 @@ pub trait CommonData {
self.get_cd_mut().text_messages.extend_with_another_messages(messages);
}
fn set_allowed_extensions(&mut self, allowed_extensions: String) {
let messages = self.get_cd_mut().allowed_extensions.set_allowed_extensions(allowed_extensions);
let messages = self.get_cd_mut().extensions.set_allowed_extensions(allowed_extensions);
self.get_cd_mut().text_messages.extend_with_another_messages(messages);
}
fn set_excluded_extensions(&mut self, excluded_extensions: String) {
let messages = self.get_cd_mut().extensions.set_excluded_extensions(excluded_extensions);
self.get_cd_mut().text_messages.extend_with_another_messages(messages);
}
@ -177,8 +181,9 @@ pub trait CommonData {
self.get_cd_mut().text_messages.extend_with_another_messages(messages);
}
fn optimize_dirs_before_start(&mut self) {
fn prepare_items(&mut self) {
let recursive_search = self.get_cd().recursive_search;
// Optimizes directories and removes recursive calls
let messages = self.get_cd_mut().directories.optimize_directories(recursive_search);
self.get_cd_mut().text_messages.extend_with_another_messages(messages);
}
@ -187,7 +192,7 @@ pub trait CommonData {
println!("---------------DEBUG PRINT COMMON---------------");
println!("Tool type: {:?}", self.get_cd().tool_type);
println!("Directories: {:?}", self.get_cd().directories);
println!("Allowed extensions: {:?}", self.get_cd().allowed_extensions);
println!("Extensions: {:?}", self.get_cd().extensions);
println!("Excluded items: {:?}", self.get_cd().excluded_items);
println!("Recursive search: {:?}", self.get_cd().recursive_search);
println!("Maximal file size: {:?}", self.get_cd().maximal_file_size);

View file

@ -1,9 +1,10 @@
use fun_time::fun_time;
use serde::Serialize;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use fun_time::fun_time;
use serde::Serialize;
pub trait DebugPrint {
fn debug_print(&self);
}

View file

@ -134,7 +134,7 @@ impl DuplicateFinder {
ignore_hard_links: true,
hash_type: HashType::Blake3,
use_prehash_cache: true,
minimal_cache_file_size: 1024 * 3256, // By default cache only >= 256 KB files
minimal_cache_file_size: 1024 * 256, // By default cache only >= 256 KB files
minimal_prehash_cache_file_size: 0,
case_sensitive_name_comparison: false,
}
@ -142,7 +142,7 @@ impl DuplicateFinder {
#[fun_time(message = "find_duplicates", level = "info")]
pub fn find_duplicates(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty();
match self.check_method {

View file

@ -1,5 +1,4 @@
use std::fs;
use std::io::prelude::*;
use crossbeam_channel::{Receiver, Sender};
@ -41,7 +40,7 @@ impl EmptyFiles {
#[fun_time(message = "find_empty_files", level = "info")]
pub fn find_empty_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
return;

View file

@ -5,12 +5,12 @@ use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use crate::common::{check_if_stop_received, prepare_thread_handler_common, send_info_and_wait_for_ending_all_threads};
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use log::debug;
use rayon::prelude::*;
use crate::common::{check_if_stop_received, prepare_thread_handler_common, send_info_and_wait_for_ending_all_threads};
use crate::common_dir_traversal::{common_get_entry_data, common_get_metadata_dir, common_read_dir, get_modified_time, CheckingMethod, ProgressData, ToolType};
use crate::common_directory::Directories;
use crate::common_items::ExcludedItems;
@ -26,6 +26,12 @@ pub struct FolderEntry {
pub modified_date: u64,
}
impl FolderEntry {
pub fn get_modified_date(&self) -> u64 {
self.modified_date
}
}
pub struct EmptyFolder {
common_data: CommonToolData,
information: Info,
@ -64,7 +70,7 @@ impl EmptyFolder {
#[fun_time(message = "find_empty_folders", level = "info")]
pub fn find_empty_folders(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
if !self.check_for_empty_folders(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
return;
@ -104,19 +110,17 @@ impl EmptyFolder {
let excluded_items = self.common_data.excluded_items.clone();
let directories = self.common_data.directories.clone();
let mut folder_entries: HashMap<String, FolderEntry> = HashMap::new();
let mut non_empty_folders: Vec<String> = vec![];
let mut start_folder_entries = Vec::with_capacity(folders_to_check.len());
let mut new_folder_entries_list = Vec::new();
for dir in &folders_to_check {
folder_entries.insert(
dir.to_string_lossy().to_string(),
FolderEntry {
path: dir.clone(),
parent_path: None,
is_empty: FolderEmptiness::Maybe,
modified_date: 0,
},
);
start_folder_entries.push(FolderEntry {
path: dir.clone(),
parent_path: None,
is_empty: FolderEmptiness::Maybe,
modified_date: 0,
});
}
while !folders_to_check.is_empty() {
@ -130,12 +134,13 @@ impl EmptyFolder {
.map(|current_folder| {
let mut dir_result = vec![];
let mut warnings = vec![];
let mut set_as_not_empty_folder_list = vec![];
let mut non_empty_folder = None;
let mut folder_entries_list = vec![];
let current_folder_as_string = current_folder.to_string_lossy().to_string();
let Some(read_dir) = common_read_dir(&current_folder, &mut warnings) else {
set_as_not_empty_folder_list.push(current_folder.to_string_lossy().to_string());
return (dir_result, warnings, set_as_not_empty_folder_list, folder_entries_list);
return (dir_result, warnings, Some(current_folder_as_string), folder_entries_list);
};
let mut counter = 0;
@ -150,23 +155,27 @@ impl EmptyFolder {
counter += 1;
Self::process_dir_in_dir_mode(
&current_folder,
&current_folder_as_string,
entry_data,
&directories,
&mut dir_result,
&mut warnings,
&excluded_items,
&mut set_as_not_empty_folder_list,
&mut non_empty_folder,
&mut folder_entries_list,
);
} else {
set_as_not_empty_folder_list.push(current_folder.to_string_lossy().to_string());
if non_empty_folder.is_none() {
non_empty_folder = Some(current_folder_as_string.clone());
}
}
}
if counter > 0 {
// Increase counter in batch, because usually it may be slow to add multiple times atomic value
atomic_counter.fetch_add(counter, Ordering::Relaxed);
}
(dir_result, warnings, set_as_not_empty_folder_list, folder_entries_list)
(dir_result, warnings, non_empty_folder, folder_entries_list)
})
.collect();
@ -174,13 +183,25 @@ impl EmptyFolder {
folders_to_check = Vec::with_capacity(required_size);
// Process collected data
for (segment, warnings, set_as_not_empty_folder_list, fe_list) in segments {
for (segment, warnings, non_empty_folder, fe_list) in segments {
folders_to_check.extend(segment);
self.common_data.text_messages.warnings.extend(warnings);
non_empty_folders.extend(set_as_not_empty_folder_list);
for (path, entry) in fe_list {
folder_entries.insert(path, entry);
if !warnings.is_empty() {
self.common_data.text_messages.warnings.extend(warnings);
}
if let Some(non_empty_folder) = non_empty_folder {
non_empty_folders.push(non_empty_folder);
}
new_folder_entries_list.push(fe_list);
}
}
let mut folder_entries: HashMap<String, FolderEntry> = HashMap::with_capacity(start_folder_entries.len() + new_folder_entries_list.iter().map(Vec::len).sum::<usize>());
for fe in start_folder_entries {
folder_entries.insert(fe.path.to_string_lossy().to_string(), fe);
}
for fe_list in new_folder_entries_list {
for fe in fe_list {
folder_entries.insert(fe.path.to_string_lossy().to_string(), fe);
}
}
@ -223,18 +244,20 @@ impl EmptyFolder {
fn process_dir_in_dir_mode(
current_folder: &Path,
current_folder_as_str: &str,
entry_data: &DirEntry,
directories: &Directories,
dir_result: &mut Vec<PathBuf>,
warnings: &mut Vec<String>,
excluded_items: &ExcludedItems,
set_as_not_empty_folder_list: &mut Vec<String>,
folder_entries_list: &mut Vec<(String, FolderEntry)>,
non_empty_folder: &mut Option<String>,
folder_entries_list: &mut Vec<FolderEntry>,
) {
let parent_folder_str = current_folder.to_string_lossy().to_string();
let next_folder = entry_data.path();
if excluded_items.is_excluded(&next_folder) || directories.is_excluded(&next_folder) {
set_as_not_empty_folder_list.push(parent_folder_str);
if non_empty_folder.is_none() {
*non_empty_folder = Some(current_folder_as_str.to_string());
}
return;
}
@ -248,20 +271,19 @@ impl EmptyFolder {
}
let Some(metadata) = common_get_metadata_dir(entry_data, warnings, &next_folder) else {
set_as_not_empty_folder_list.push(parent_folder_str);
if non_empty_folder.is_none() {
*non_empty_folder = Some(current_folder_as_str.to_string());
}
return;
};
dir_result.push(next_folder.clone());
folder_entries_list.push((
next_folder.to_string_lossy().to_string(),
FolderEntry {
path: next_folder,
parent_path: Some(parent_folder_str),
is_empty: FolderEmptiness::Maybe,
modified_date: get_modified_time(&metadata, warnings, current_folder, true),
},
));
folder_entries_list.push(FolderEntry {
path: next_folder,
parent_path: Some(current_folder_as_str.to_string()),
is_empty: FolderEmptiness::Maybe,
modified_date: get_modified_time(&metadata, warnings, current_folder, true),
});
}
#[fun_time(message = "delete_files", level = "debug")]

View file

@ -1,5 +1,4 @@
use std::fs;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
@ -73,7 +72,7 @@ impl InvalidSymlinks {
#[fun_time(message = "find_invalid_links", level = "info")]
pub fn find_invalid_links(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
return;

View file

@ -2,14 +2,12 @@ use std::cmp::max;
use std::collections::{BTreeMap, HashSet};
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::{mem, panic};
use anyhow::Context;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use humansize::{format_size, BINARY};
@ -138,7 +136,7 @@ impl SameMusic {
#[fun_time(message = "find_same_music", level = "info")]
pub fn find_same_music(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -177,8 +175,8 @@ impl SameMusic {
#[fun_time(message = "check_files", level = "debug")]
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
self.common_data.allowed_extensions.set_and_validate_extensions(AUDIO_FILES_EXTENSIONS);
if !self.common_data.allowed_extensions.set_any_extensions() {
self.common_data.extensions.set_and_validate_allowed_extensions(AUDIO_FILES_EXTENSIONS);
if !self.common_data.extensions.set_any_extensions() {
return true;
}

View file

@ -6,7 +6,6 @@ use std::time::SystemTime;
use std::{mem, panic};
use bk_tree::BKTree;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use humansize::{format_size, BINARY};
@ -41,7 +40,8 @@ pub const SIMILAR_VALUES: [[u32; 6]; 4] = [
pub struct ImagesEntry {
pub path: PathBuf,
pub size: u64,
pub dimensions: String,
pub width: u32,
pub height: u32,
pub modified_date: u64,
pub hash: ImHash,
pub similarity: u32,
@ -66,7 +66,8 @@ impl FileEntry {
path: self.path,
modified_date: self.modified_date,
dimensions: String::new(),
width: 0,
height: 0,
hash: Vec::new(),
similarity: 0,
image_type: ImageType::Unknown,
@ -152,7 +153,7 @@ impl SimilarImages {
#[fun_time(message = "find_similar_images", level = "info")]
pub fn find_similar_images(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty();
if !self.check_for_similar_images(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -174,14 +175,14 @@ impl SimilarImages {
fn check_for_similar_images(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
if cfg!(feature = "heif") {
self.common_data
.allowed_extensions
.set_and_validate_extensions(&[IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS, HEIC_EXTENSIONS].concat());
.extensions
.set_and_validate_allowed_extensions(&[IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS, HEIC_EXTENSIONS].concat());
} else {
self.common_data
.allowed_extensions
.set_and_validate_extensions(&[IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS].concat());
.extensions
.set_and_validate_allowed_extensions(&[IMAGE_RS_SIMILAR_IMAGES_EXTENSIONS, RAW_IMAGE_EXTENSIONS].concat());
}
if !self.common_data.allowed_extensions.set_any_extensions() {
if !self.common_data.extensions.set_any_extensions() {
return true;
}
@ -294,7 +295,7 @@ impl SimilarImages {
.while_some()
.filter_map(|e| e)
.collect::<Vec<ImagesEntry>>();
debug!("hash_images - end hashing images");
debug!("hash_images - end hashing {} images", vec_file_entry.len());
send_info_and_wait_for_ending_all_threads(&progress_thread_run, progress_thread_handle);
@ -396,7 +397,8 @@ impl SimilarImages {
let dimensions = img.dimensions();
file_entry.dimensions = format!("{}x{}", dimensions.0, dimensions.1);
file_entry.width = dimensions.0;
file_entry.height = dimensions.1;
let hasher_config = HasherConfig::new()
.hash_size(self.hash_size as u32, self.hash_size as u32)
@ -460,54 +462,21 @@ impl SimilarImages {
collected_similar_images: &mut HashMap<ImHash, Vec<ImagesEntry>>,
hashes_similarity: HashMap<ImHash, (ImHash, u32)>,
) {
if self.common_data.use_reference_folders {
// This is same step as without reference folders, but also checks if children are inside/outside reference directories, because may happen, that one file is inside reference folder and other outside
// Collecting results to vector
for (parent_hash, child_number) in hashes_parents {
// If hash contains other hasher OR multiple images are available for checked hash
if child_number > 0 || hashes_with_multiple_images.contains(&parent_hash) {
let vec_fe = all_hashed_images[&parent_hash].clone();
collected_similar_images.insert(parent_hash.clone(), vec_fe);
}
}
// Collecting results to vector
for (parent_hash, child_number) in hashes_parents {
// If hash contains other hasher OR multiple images are available for checked hash
if child_number > 0 || hashes_with_multiple_images.contains(&parent_hash) {
let vec_fe = all_hashed_images
.get(&parent_hash)
.unwrap()
.iter()
.filter(|e| is_in_reference_folder(&self.common_data.directories.reference_directories, &e.path))
.cloned()
.collect();
collected_similar_images.insert(parent_hash.clone(), vec_fe);
}
}
for (child_hash, (parent_hash, similarity)) in hashes_similarity {
let mut vec_fe: Vec<_> = all_hashed_images
.get(&child_hash)
.unwrap()
.iter()
.filter(|e| !is_in_reference_folder(&self.common_data.directories.reference_directories, &e.path))
.cloned()
.collect();
for fe in &mut vec_fe {
fe.similarity = similarity;
}
collected_similar_images.get_mut(&parent_hash).unwrap().append(&mut vec_fe);
}
} else {
// Collecting results to vector
for (parent_hash, child_number) in hashes_parents {
// If hash contains other hasher OR multiple images are available for checked hash
if child_number > 0 || hashes_with_multiple_images.contains(&parent_hash) {
let vec_fe = all_hashed_images.get(&parent_hash).unwrap().clone();
collected_similar_images.insert(parent_hash.clone(), vec_fe);
}
}
for (child_hash, (parent_hash, similarity)) in hashes_similarity {
let mut vec_fe = all_hashed_images.get(&child_hash).unwrap().clone();
for fe in &mut vec_fe {
fe.similarity = similarity;
}
collected_similar_images.get_mut(&parent_hash).unwrap().append(&mut vec_fe);
for (child_hash, (parent_hash, similarity)) in hashes_similarity {
let mut vec_fe = all_hashed_images[&child_hash].clone();
for fe in &mut vec_fe {
fe.similarity = similarity;
}
collected_similar_images.get_mut(&parent_hash).unwrap().append(&mut vec_fe);
}
}
@ -773,7 +742,7 @@ impl SimilarImages {
}
}
}
assert!(!found, "Found Invalid entries, verify errors before"); // TODO crashes with empty result with reference folder, verify why
assert!(!found, "Found Invalid entries, verify errors before");
}
fn delete_files(&mut self) {
@ -818,9 +787,10 @@ impl PrintResults for SimilarImages {
for file_entry in struct_similar {
writeln!(
writer,
"{:?} - {} - {} - {}",
"{:?} - {}x{} - {} - {}",
file_entry.path,
file_entry.dimensions,
file_entry.width,
file_entry.height,
format_size(file_entry.size, BINARY),
get_string_from_similarity(&file_entry.similarity, self.hash_size)
)?;
@ -835,18 +805,20 @@ impl PrintResults for SimilarImages {
writeln!(writer)?;
writeln!(
writer,
"{:?} - {} - {} - {}",
"{:?} - {}x{} - {} - {}",
file_entry.path,
file_entry.dimensions,
file_entry.width,
file_entry.height,
format_size(file_entry.size, BINARY),
get_string_from_similarity(&file_entry.similarity, self.hash_size)
)?;
for file_entry in vec_file_entry {
writeln!(
writer,
"{:?} - {} - {} - {}",
"{:?} - {}x{} - {} - {}",
file_entry.path,
file_entry.dimensions,
file_entry.width,
file_entry.height,
format_size(file_entry.size, BINARY),
get_string_from_similarity(&file_entry.similarity, self.hash_size)
)?;
@ -1007,12 +979,12 @@ fn debug_check_for_duplicated_things(
for (hash, number_of_children) in hashes_parents {
if *number_of_children > 0 {
if hashmap_hashes.contains(hash) {
println!("------1--HASH--{} {:?}", numm, all_hashed_images.get(hash).unwrap());
println!("------1--HASH--{} {:?}", numm, all_hashed_images[hash]);
found_broken_thing = true;
}
hashmap_hashes.insert((*hash).clone());
for i in all_hashed_images.get(hash).unwrap() {
for i in &all_hashed_images[hash] {
let name = i.path.to_string_lossy().to_string();
if hashmap_names.contains(&name) {
println!("------1--NAME--{numm} {name:?}");
@ -1024,12 +996,12 @@ fn debug_check_for_duplicated_things(
}
for hash in hashes_similarity.keys() {
if hashmap_hashes.contains(hash) {
println!("------2--HASH--{} {:?}", numm, all_hashed_images.get(hash).unwrap());
println!("------2--HASH--{} {:?}", numm, all_hashed_images[hash]);
found_broken_thing = true;
}
hashmap_hashes.insert((*hash).clone());
for i in all_hashed_images.get(hash).unwrap() {
for i in &all_hashed_images[hash] {
let name = i.path.to_string_lossy().to_string();
if hashmap_names.contains(&name) {
println!("------2--NAME--{numm} {name:?}");
@ -1103,9 +1075,9 @@ mod tests {
use std::collections::HashMap;
use std::path::PathBuf;
use crate::common_dir_traversal::ToolType;
use bk_tree::BKTree;
use crate::common_dir_traversal::ToolType;
use crate::common_directory::Directories;
use crate::common_tool::CommonToolData;
use crate::similar_images::{Hamming, ImHash, ImageType, ImagesEntry, SimilarImages};
@ -1201,34 +1173,6 @@ mod tests {
}
}
// TODO this not works yet,
// Need to find a way to
// #[test]
// fn test_similar_similarity() {
// for _ in 0..100 {
// let mut similar_images = SimilarImages {
// similarity: 10,
// common_data: CommonToolData {
// tool_type: ToolType::SimilarImages,
// ..Default::default()
// },
// use_reference_folders: false,
// ..Default::default()
// };
//
// let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0000_0001], "abc.txt");
// let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0000_0010], "bcd.txt");
// let fe3 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0000_0100], "rrd.txt");
// let fe4 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 0b0111_1111], "rdd.txt");
//
// add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2, fe3, fe4]);
//
// similar_images.find_similar_hashes(None, None);
// assert_eq!(similar_images.get_similar_images().len(), 1);
// assert_eq!(similar_images.get_similar_images()[0].len(), 4);
// }
// }
#[test]
fn test_simple_referenced_same_group() {
for _ in 0..100 {
@ -1473,6 +1417,35 @@ mod tests {
}
}
#[test]
fn test_reference_same() {
for _ in 0..100 {
let mut similar_images = SimilarImages {
similarity: 1,
common_data: CommonToolData {
tool_type: ToolType::SimilarImages,
directories: Directories {
reference_directories: vec![PathBuf::from("/home/rr/")],
..Default::default()
},
use_reference_folders: true,
..Default::default()
},
..Default::default()
};
let fe1 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/rr/abc.txt");
let fe2 = create_random_file_entry(vec![1, 1, 1, 1, 1, 1, 1, 1], "/home/kk/bcd.txt");
add_hashes(&mut similar_images.image_hashes, vec![fe1, fe2]);
similar_images.find_similar_hashes(None, None);
let res = similar_images.get_similar_images_referenced();
assert_eq!(res.len(), 1);
assert_eq!(res[0].1.len(), 1);
}
}
#[test]
fn test_reference_union() {
for _ in 0..100 {
@ -1544,7 +1517,8 @@ mod tests {
ImagesEntry {
path: PathBuf::from(name.to_string()),
size: 0,
dimensions: String::new(),
width: 100,
height: 100,
modified_date: 0,
hash,
similarity: 0,

View file

@ -20,7 +20,6 @@ use crate::common_dir_traversal::{inode, take_1_per_inode, CheckingMethod, DirTr
use crate::common_tool::{CommonData, CommonToolData, DeleteMethod};
use crate::common_traits::{DebugPrint, PrintResults, ResultEntry};
use crate::flc;
use crate::localizer_core::generate_translation_hashmap;
pub const MAX_TOLERANCE: i32 = 20;
@ -123,12 +122,12 @@ impl SimilarVideos {
#[cfg(target_os = "windows")]
self.common_data.text_messages.errors.push(flc!("core_ffmpeg_not_found_windows"));
#[cfg(target_os = "linux")]
self.common_data.text_messages.errors.push(flc!(
"core_ffmpeg_missing_in_snap",
generate_translation_hashmap(vec![("url", "https://github.com/snapcrafters/ffmpeg/issues/73".to_string())])
));
self.common_data
.text_messages
.errors
.push(flc!("core_ffmpeg_missing_in_snap", url = "https://github.com/snapcrafters/ffmpeg/issues/73"));
} else {
self.optimize_dirs_before_start();
self.prepare_items();
self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty();
if !self.check_for_similar_videos(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -145,8 +144,8 @@ impl SimilarVideos {
// #[fun_time(message = "check_for_similar_videos", level = "debug")]
fn check_for_similar_videos(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
self.common_data.allowed_extensions.set_and_validate_extensions(VIDEO_FILES_EXTENSIONS);
if !self.common_data.allowed_extensions.set_any_extensions() {
self.common_data.extensions.set_and_validate_allowed_extensions(VIDEO_FILES_EXTENSIONS);
if !self.common_data.extensions.set_any_extensions() {
return true;
}

View file

@ -1,7 +1,6 @@
use std::fs;
use std::fs::DirEntry;
use std::io::prelude::*;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
@ -60,7 +59,7 @@ impl Temporary {
#[fun_time(message = "find_temporary_files", level = "info")]
pub fn find_temporary_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.prepare_items();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
return;

View file

@ -3,19 +3,19 @@ name = "czkawka_gui"
version = "6.1.0"
authors = ["Rafał Mikrut <mikrutrafal@protonmail.com>"]
edition = "2021"
rust-version = "1.72.1"
rust-version = "1.74.0"
description = "GTK frontend of Czkawka"
license = "MIT"
homepage = "https://github.com/qarmin/czkawka"
repository = "https://github.com/qarmin/czkawka"
[dependencies]
gdk4 = "0.7"
glib = "0.18"
gtk4 = { version = "0.7", default-features = false, features = ["v4_6"] }
gdk4 = "0.8"
glib = "0.19"
gtk4 = { version = "0.8", default-features = false, features = ["v4_6"] }
humansize = "2.1"
chrono = "0.4.31"
chrono = "0.4.34"
# Used for sending stop signal across threads
crossbeam-channel = "0.5"
@ -36,14 +36,14 @@ regex = "1.10"
image_hasher = "1.2"
# Move files to trash
trash = "3.2"
trash = "3.3"
# For moving files(why std::fs doesn't have such features?)
fs_extra = "1.3"
# Language
i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.7"
i18n-embed-fl = "0.8"
rust-embed = { version = "8.2", features = ["debug-embed"] }
once_cell = "1.19"

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2023 Rafał Mikrut
Copyright (c) 2020-2024 Rafał Mikrut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -36,7 +36,7 @@ Compiling the gui is harder than compiling cli or core, because it uses gtk4 whi
### Requirements
| Program | Minimal version |
|:---------:|:-----------------:|
| Rust | 1.72.1 |
| Rust | 1.74.0 |
| GTK | 4.6 |
### Linux (Ubuntu, but on other OS should work similar)

View file

@ -192,12 +192,18 @@ upper_allowed_extensions_tooltip =
Usage example ".exe, IMAGE, VIDEO, .rar, 7z" - this means that images (e.g. jpg, png), videos (e.g. avi, mp4), exe, rar, and 7z files will be scanned.
upper_excluded_extensions_tooltip =
List of disabled files which will be ignored in scan.
When using both allowed and disabled extensions, this one has higher priority, so file will not be checked.
upper_excluded_items_tooltip =
Excluded items must contain * wildcard and should be separated by commas.
This is slower than Excluded Directories, so use it carefully.
upper_excluded_items = Excluded Items:
upper_allowed_extensions = Allowed Extensions:
upper_excluded_extensions = Disabled Extensions:
# Popovers

View file

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
enable-background="new 0 0 512 512"
viewBox="0 0 512 512"
version="1.1"
id="svg16"
sodipodi:docname="czk_hide_down.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
enable-background="new 0 0 512 512"
viewBox="0 0 512 512"
version="1.1"
id="svg16"
sodipodi:docname="czk_hide_down.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
>
<defs
id="defs20" />
<sodipodi:namedview

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
enable-background="new 0 0 512 512"
viewBox="0 0 512 512"
version="1.1"
id="svg16"
sodipodi:docname="czk_hide_up.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
enable-background="new 0 0 512 512"
viewBox="0 0 512 512"
version="1.1"
id="svg16"
sodipodi:docname="czk_hide_up.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
>
<defs
id="defs20" />
<sodipodi:namedview

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<polygon style="fill:#298983;" points="358.206,345.663 357.688,227.522 292.942,227.806 293.462,345.948 325.975,378.178 "/>
<rect x="359.234" y="336.67" transform="matrix(0.7102 0.704 -0.704 0.7102 381.6591 -165.2999)" style="fill:#A5F2E5;" width="64.748" height="88.511"/>
<path d="M462.868,366.489l-57.464-56.96c-1.514-1.501-3.559-2.342-5.69-2.342c-0.012,0-0.024,0-0.036,0

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -1,10 +1,11 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::Duration;
use chrono::NaiveDateTime;
use crossbeam_channel::Receiver;
use fun_time::fun_time;
use glib::Receiver;
use gtk4::prelude::*;
use gtk4::{Entry, ListStore, TextView, TreeView, Widget};
use humansize::{format_size, BINARY};
@ -21,7 +22,6 @@ use czkawka_core::duplicate::DuplicateFinder;
use czkawka_core::empty_files::EmptyFiles;
use czkawka_core::empty_folder::EmptyFolder;
use czkawka_core::invalid_symlinks::InvalidSymlinks;
use czkawka_core::localizer_core::generate_translation_hashmap;
use czkawka_core::same_music::{MusicSimilarity, SameMusic};
use czkawka_core::similar_images;
use czkawka_core::similar_images::{ImagesEntry, SimilarImages};
@ -36,7 +36,7 @@ use crate::notebook_enums::*;
use crate::notebook_info::NOTEBOOKS_INFO;
use crate::opening_selecting_records::*;
pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver<Message>) {
pub fn connect_compute_results(gui_data: &GuiData, result_receiver: Receiver<Message>) {
let combo_box_image_hash_size = gui_data.main_notebook.combo_box_image_hash_size.clone();
let buttons_search = gui_data.bottom_buttons.buttons_search.clone();
let notebook_main = gui_data.main_notebook.notebook_main.clone();
@ -76,163 +76,171 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver<
let main_context = glib::MainContext::default();
let _guard = main_context.acquire().unwrap();
glib_stop_receiver.attach(None, move |msg| {
buttons_search.show();
glib::spawn_future_local(async move {
loop {
loop {
let msg = result_receiver.try_recv();
if let Ok(msg) = msg {
buttons_search.show();
notebook_main.set_sensitive(true);
notebook_upper.set_sensitive(true);
button_settings.set_sensitive(true);
button_app_info.set_sensitive(true);
notebook_main.set_sensitive(true);
notebook_upper.set_sensitive(true);
button_settings.set_sensitive(true);
button_app_info.set_sensitive(true);
window_progress.hide();
window_progress.hide();
taskbar_state.borrow().hide();
taskbar_state.borrow().hide();
let hash_size_index = combo_box_image_hash_size.active().unwrap() as usize;
let hash_size = IMAGES_HASH_SIZE_COMBO_BOX[hash_size_index] as u8;
let hash_size_index = combo_box_image_hash_size.active().unwrap() as usize;
let hash_size = IMAGES_HASH_SIZE_COMBO_BOX[hash_size_index] as u8;
match msg {
Message::Duplicates(df) => {
computer_duplicate_finder(
df,
&entry_info,
&tree_view_duplicate_finder,
&text_view_errors,
&shared_duplication_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::EmptyFolders(ef) => {
computer_empty_folders(
ef,
&entry_info,
&tree_view_empty_folder_finder,
&text_view_errors,
&shared_empty_folders_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::EmptyFiles(vf) => {
computer_empty_files(
vf,
&entry_info,
&tree_view_empty_files_finder,
&text_view_errors,
&shared_empty_files_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::BigFiles(bf) => {
computer_big_files(
bf,
&entry_info,
&tree_view_big_files_finder,
&text_view_errors,
&shared_big_files_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::Temporary(tf) => {
computer_temporary_files(
tf,
&entry_info,
&tree_view_temporary_files_finder,
&text_view_errors,
&shared_temporary_files_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::SimilarImages(sf) => {
computer_similar_images(
sf,
&entry_info,
&tree_view_similar_images_finder,
&text_view_errors,
&shared_similar_images_state,
&shared_buttons,
&buttons_array,
&buttons_names,
hash_size,
);
}
Message::SimilarVideos(ff) => {
computer_similar_videos(
ff,
&entry_info,
&tree_view_similar_videos_finder,
&text_view_errors,
&shared_similar_videos_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::SameMusic(mf) => {
computer_same_music(
mf,
&entry_info,
&tree_view_same_music_finder,
&text_view_errors,
&shared_same_music_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::InvalidSymlinks(ifs) => {
computer_invalid_symlinks(
ifs,
&entry_info,
&tree_view_invalid_symlinks,
&text_view_errors,
&shared_same_invalid_symlinks,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::BrokenFiles(br) => {
computer_broken_files(
br,
&entry_info,
&tree_view_broken_files,
&text_view_errors,
&shared_broken_files_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::BadExtensions(be) => {
computer_bad_extensions(
be,
&entry_info,
&tree_view_bad_extensions,
&text_view_errors,
&shared_bad_extensions_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
match msg {
Message::Duplicates(df) => {
compute_duplicate_finder(
df,
&entry_info,
&tree_view_duplicate_finder,
&text_view_errors,
&shared_duplication_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::EmptyFolders(ef) => {
compute_empty_folders(
ef,
&entry_info,
&tree_view_empty_folder_finder,
&text_view_errors,
&shared_empty_folders_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::EmptyFiles(vf) => {
compute_empty_files(
vf,
&entry_info,
&tree_view_empty_files_finder,
&text_view_errors,
&shared_empty_files_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::BigFiles(bf) => {
compute_big_files(
bf,
&entry_info,
&tree_view_big_files_finder,
&text_view_errors,
&shared_big_files_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::Temporary(tf) => {
compute_temporary_files(
tf,
&entry_info,
&tree_view_temporary_files_finder,
&text_view_errors,
&shared_temporary_files_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::SimilarImages(sf) => {
compute_similar_images(
sf,
&entry_info,
&tree_view_similar_images_finder,
&text_view_errors,
&shared_similar_images_state,
&shared_buttons,
&buttons_array,
&buttons_names,
hash_size,
);
}
Message::SimilarVideos(ff) => {
compute_similar_videos(
ff,
&entry_info,
&tree_view_similar_videos_finder,
&text_view_errors,
&shared_similar_videos_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::SameMusic(mf) => {
compute_same_music(
mf,
&entry_info,
&tree_view_same_music_finder,
&text_view_errors,
&shared_same_music_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::InvalidSymlinks(ifs) => {
compute_invalid_symlinks(
ifs,
&entry_info,
&tree_view_invalid_symlinks,
&text_view_errors,
&shared_same_invalid_symlinks,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::BrokenFiles(br) => {
compute_broken_files(
br,
&entry_info,
&tree_view_broken_files,
&text_view_errors,
&shared_broken_files_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
Message::BadExtensions(be) => {
compute_bad_extensions(
be,
&entry_info,
&tree_view_bad_extensions,
&text_view_errors,
&shared_bad_extensions_state,
&shared_buttons,
&buttons_array,
&buttons_names,
);
}
}
} else {
break;
}
}
glib::timeout_future(Duration::from_millis(300)).await;
}
// Returning false here would close the receiver and have senders fail
glib::ControlFlow::Continue
});
}
#[fun_time(message = "computer_bad_extensions", level = "debug")]
fn computer_bad_extensions(
#[fun_time(message = "compute_bad_extensions", level = "debug")]
fn compute_bad_extensions(
be: BadExtensions,
entry_info: &Entry,
tree_view: &TreeView,
@ -250,13 +258,7 @@ fn computer_bad_extensions(
let text_messages = be.get_text_messages();
let bad_extensions_number: usize = information.number_of_files_with_bad_extension;
entry_info.set_text(
flg!(
"compute_found_bad_extensions",
generate_translation_hashmap(vec![("number_files", bad_extensions_number.to_string()),])
)
.as_str(),
);
entry_info.set_text(flg!("compute_found_bad_extensions", number_files = bad_extensions_number).as_str());
// Create GUI
{
@ -302,8 +304,8 @@ fn computer_bad_extensions(
}
}
#[fun_time(message = "computer_broken_files", level = "debug")]
fn computer_broken_files(
#[fun_time(message = "compute_broken_files", level = "debug")]
fn compute_broken_files(
br: BrokenFiles,
entry_info: &Entry,
tree_view: &TreeView,
@ -322,13 +324,7 @@ fn computer_broken_files(
let broken_files_number: usize = information.number_of_broken_files;
entry_info.set_text(
flg!(
"compute_found_broken_files",
generate_translation_hashmap(vec![("number_files", broken_files_number.to_string()),])
)
.as_str(),
);
entry_info.set_text(flg!("compute_found_broken_files", number_files = broken_files_number).as_str());
// Create GUI
{
@ -373,8 +369,8 @@ fn computer_broken_files(
}
}
#[fun_time(message = "computer_invalid_symlinks", level = "debug")]
fn computer_invalid_symlinks(
#[fun_time(message = "compute_invalid_symlinks", level = "debug")]
fn compute_invalid_symlinks(
ifs: InvalidSymlinks,
entry_info: &Entry,
tree_view: &TreeView,
@ -393,13 +389,7 @@ fn computer_invalid_symlinks(
let invalid_symlinks: usize = information.number_of_invalid_symlinks;
entry_info.set_text(
flg!(
"compute_found_invalid_symlinks",
generate_translation_hashmap(vec![("number_files", invalid_symlinks.to_string()),])
)
.as_str(),
);
entry_info.set_text(flg!("compute_found_invalid_symlinks", number_files = invalid_symlinks).as_str());
// Create GUI
{
@ -445,8 +435,8 @@ fn computer_invalid_symlinks(
}
}
#[fun_time(message = "computer_same_music", level = "debug")]
fn computer_same_music(
#[fun_time(message = "compute_same_music", level = "debug")]
fn compute_same_music(
mf: SameMusic,
entry_info: &Entry,
tree_view: &TreeView,
@ -473,10 +463,8 @@ fn computer_same_music(
entry_info.set_text(
flg!(
"compute_found_music",
generate_translation_hashmap(vec![
("number_files", information.number_of_duplicates.to_string()),
("number_groups", information.number_of_groups.to_string()),
])
number_files = information.number_of_duplicates,
number_groups = information.number_of_groups
)
.as_str(),
);
@ -614,8 +602,8 @@ fn computer_same_music(
}
}
#[fun_time(message = "computer_similar_videos", level = "debug")]
fn computer_similar_videos(
#[fun_time(message = "compute_similar_videos", level = "debug")]
fn compute_similar_videos(
ff: SimilarVideos,
entry_info: &Entry,
tree_view: &TreeView,
@ -640,10 +628,8 @@ fn computer_similar_videos(
entry_info.set_text(
flg!(
"compute_found_videos",
generate_translation_hashmap(vec![
("number_files", information.number_of_duplicates.to_string()),
("number_groups", information.number_of_groups.to_string()),
])
number_files = information.number_of_duplicates,
number_groups = information.number_of_groups
)
.as_str(),
);
@ -710,8 +696,8 @@ fn computer_similar_videos(
}
}
#[fun_time(message = "computer_similar_images", level = "debug")]
fn computer_similar_images(
#[fun_time(message = "compute_similar_images", level = "debug")]
fn compute_similar_images(
sf: SimilarImages,
entry_info: &Entry,
tree_view: &TreeView,
@ -738,10 +724,8 @@ fn computer_similar_images(
entry_info.set_text(
flg!(
"compute_found_images",
generate_translation_hashmap(vec![
("number_files", information.number_of_duplicates.to_string()),
("number_groups", information.number_of_groups.to_string()),
])
number_files = information.number_of_duplicates,
number_groups = information.number_of_groups
)
.as_str(),
);
@ -771,7 +755,7 @@ fn computer_similar_images(
&directory,
base_file_entry.size,
base_file_entry.modified_date,
&base_file_entry.dimensions,
&format!("{}x{}", base_file_entry.width, base_file_entry.height),
0,
hash_size,
true,
@ -785,7 +769,7 @@ fn computer_similar_images(
&directory,
file_entry.size,
file_entry.modified_date,
&file_entry.dimensions,
&format!("{}x{}", file_entry.width, file_entry.height),
file_entry.similarity,
hash_size,
false,
@ -815,7 +799,7 @@ fn computer_similar_images(
&directory,
file_entry.size,
file_entry.modified_date,
&file_entry.dimensions,
&format!("{}x{}", file_entry.width, file_entry.height),
file_entry.similarity,
hash_size,
false,
@ -843,8 +827,8 @@ fn computer_similar_images(
}
}
#[fun_time(message = "computer_temporary_files", level = "debug")]
fn computer_temporary_files(
#[fun_time(message = "compute_temporary_files", level = "debug")]
fn compute_temporary_files(
tf: Temporary,
entry_info: &Entry,
tree_view: &TreeView,
@ -862,13 +846,7 @@ fn computer_temporary_files(
let text_messages = tf.get_text_messages();
let temporary_files_number: usize = information.number_of_temporary_files;
entry_info.set_text(
flg!(
"compute_found_temporary_files",
generate_translation_hashmap(vec![("number_files", temporary_files_number.to_string()),])
)
.as_str(),
);
entry_info.set_text(flg!("compute_found_temporary_files", number_files = temporary_files_number).as_str());
// Create GUI
{
@ -912,8 +890,8 @@ fn computer_temporary_files(
}
}
#[fun_time(message = "computer_big_files", level = "debug")]
fn computer_big_files(
#[fun_time(message = "compute_big_files", level = "debug")]
fn compute_big_files(
bf: BigFile,
entry_info: &Entry,
tree_view: &TreeView,
@ -932,13 +910,7 @@ fn computer_big_files(
let biggest_files_number: usize = information.number_of_real_files;
entry_info.set_text(
flg!(
"compute_found_big_files",
generate_translation_hashmap(vec![("number_files", biggest_files_number.to_string()),])
)
.as_str(),
);
entry_info.set_text(flg!("compute_found_big_files", number_files = biggest_files_number).as_str());
// Create GUI
{
@ -980,8 +952,8 @@ fn computer_big_files(
}
}
#[fun_time(message = "computer_empty_files", level = "debug")]
fn computer_empty_files(
#[fun_time(message = "compute_empty_files", level = "debug")]
fn compute_empty_files(
vf: EmptyFiles,
entry_info: &Entry,
tree_view: &TreeView,
@ -1000,13 +972,7 @@ fn computer_empty_files(
let empty_files_number: usize = information.number_of_empty_files;
entry_info.set_text(
flg!(
"compute_found_empty_files",
generate_translation_hashmap(vec![("number_files", empty_files_number.to_string()),])
)
.as_str(),
);
entry_info.set_text(flg!("compute_found_empty_files", number_files = empty_files_number).as_str());
// Create GUI
{
@ -1047,8 +1013,8 @@ fn computer_empty_files(
}
}
#[fun_time(message = "computer_empty_folders", level = "debug")]
fn computer_empty_folders(
#[fun_time(message = "compute_empty_folders", level = "debug")]
fn compute_empty_folders(
ef: EmptyFolder,
entry_info: &Entry,
tree_view: &TreeView,
@ -1067,13 +1033,7 @@ fn computer_empty_folders(
let empty_folder_number: usize = information.number_of_empty_folders;
entry_info.set_text(
flg!(
"compute_found_empty_folders",
generate_translation_hashmap(vec![("number_files", empty_folder_number.to_string()),])
)
.as_str(),
);
entry_info.set_text(flg!("compute_found_empty_folders", number_files = empty_folder_number).as_str());
// Create GUI
{
@ -1116,8 +1076,8 @@ fn computer_empty_folders(
}
}
#[fun_time(message = "computer_duplicate_finder", level = "debug")]
fn computer_duplicate_finder(
#[fun_time(message = "compute_duplicate_finder", level = "debug")]
fn compute_duplicate_finder(
df: DuplicateFinder,
entry_info: &Entry,
tree_view_duplicate_finder: &TreeView,
@ -1167,22 +1127,14 @@ fn computer_duplicate_finder(
_ => panic!(),
}
if duplicates_size == 0 {
entry_info.set_text(
flg!(
"compute_found_duplicates_name",
generate_translation_hashmap(vec![("number_files", duplicates_number.to_string()), ("number_groups", duplicates_group.to_string())])
)
.as_str(),
);
entry_info.set_text(flg!("compute_found_duplicates_name", number_files = duplicates_number, number_groups = duplicates_group).as_str());
} else {
entry_info.set_text(
flg!(
"compute_found_duplicates_hash_size",
generate_translation_hashmap(vec![
("number_files", duplicates_number.to_string()),
("number_groups", duplicates_group.to_string()),
("size", format_size(duplicates_size, BINARY))
])
number_files = duplicates_number,
number_groups = duplicates_group,
size = format_size(duplicates_size, BINARY)
)
.as_str(),
);

View file

@ -15,7 +15,6 @@ use crate::gui_structs::gui_data::GuiData;
use crate::help_functions::{
count_number_of_groups, get_all_direct_children, get_full_name_from_path_name, get_max_file_name, get_pixbuf_from_dynamic_image, resize_pixbuf_dimension,
};
use crate::localizer_core::generate_translation_hashmap;
use crate::notebook_info::{NotebookObject, NOTEBOOKS_INFO};
const BIG_PREVIEW_SIZE: i32 = 600;
@ -303,11 +302,9 @@ fn populate_groups_at_start(
label_group_info.set_text(
flg!(
"compare_groups_number",
generate_translation_hashmap(vec![
("current_group", current_group.to_string()),
("all_groups", group_number.to_string()),
("images_in_group", cache_all_images.len().to_string())
])
current_group = current_group,
all_groups = group_number,
images_in_group = cache_all_images.len()
)
.as_str(),
);

View file

@ -8,7 +8,6 @@ use gtk4::{Align, CheckButton, Dialog, Orientation, ResponseType, TextView};
use crate::flg;
use crate::gui_structs::gui_data::GuiData;
use crate::help_functions::*;
use crate::localizer_core::generate_translation_hashmap;
use crate::notebook_enums::*;
use crate::notebook_info::NOTEBOOKS_INFO;
@ -148,13 +147,11 @@ fn create_dialog_ask_for_deletion(window_main: &gtk4::Window, number_of_selected
let label: gtk4::Label = gtk4::Label::new(Some(&flg!("delete_question_label")));
let label2: gtk4::Label = match number_of_selected_groups {
0 => gtk4::Label::new(Some(&flg!(
"delete_items_label",
generate_translation_hashmap(vec![("items", number_of_selected_items.to_string())])
))),
0 => gtk4::Label::new(Some(&flg!("delete_items_label", items = number_of_selected_items))),
_ => gtk4::Label::new(Some(&flg!(
"delete_items_groups_label",
generate_translation_hashmap(vec![("items", number_of_selected_items.to_string()), ("groups", number_of_selected_groups.to_string())])
items = number_of_selected_items,
groups = number_of_selected_groups
))),
};
@ -368,10 +365,7 @@ pub fn empty_folder_remover(
}
}
if error_happened {
messages += &flg!(
"delete_folder_failed",
generate_translation_hashmap(vec![("dir", get_full_name_from_path_name(&path, &name))])
);
messages += &flg!("delete_folder_failed", dir = get_full_name_from_path_name(&path, &name));
messages += "\n";
}
}
@ -425,11 +419,7 @@ pub fn basic_remove(
}
Err(e) => {
messages += flg!(
"delete_file_failed",
generate_translation_hashmap(vec![("name", get_full_name_from_path_name(&path, &name)), ("reason", e.to_string())])
)
.as_str();
messages += flg!("delete_file_failed", name = get_full_name_from_path_name(&path, &name), reason = e.to_string()).as_str();
messages += "\n";
}
}
@ -439,11 +429,7 @@ pub fn basic_remove(
model.remove(&iter);
}
Err(e) => {
messages += flg!(
"delete_file_failed",
generate_translation_hashmap(vec![("name", get_full_name_from_path_name(&path, &name)), ("reason", e.to_string())])
)
.as_str();
messages += flg!("delete_file_failed", name = get_full_name_from_path_name(&path, &name), reason = e.to_string()).as_str();
messages += "\n";
}
}
@ -513,19 +499,11 @@ pub fn tree_remove(
for file_name in vec_file_name {
if !use_trash {
if let Err(e) = fs::remove_file(get_full_name_from_path_name(&path, &file_name)) {
messages += flg!(
"delete_file_failed",
generate_translation_hashmap(vec![("name", get_full_name_from_path_name(&path, &file_name)), ("reason", e.to_string())])
)
.as_str();
messages += flg!("delete_file_failed", name = get_full_name_from_path_name(&path, &file_name), reason = e.to_string()).as_str();
messages += "\n";
}
} else if let Err(e) = trash::delete(get_full_name_from_path_name(&path, &file_name)) {
messages += flg!(
"delete_file_failed",
generate_translation_hashmap(vec![("name", get_full_name_from_path_name(&path, &file_name)), ("reason", e.to_string())])
)
.as_str();
messages += flg!("delete_file_failed", name = get_full_name_from_path_name(&path, &file_name), reason = e.to_string()).as_str();
messages += "\n";
}

View file

@ -9,7 +9,6 @@ use czkawka_core::duplicate::make_hard_link;
use crate::flg;
use crate::gui_structs::gui_data::GuiData;
use crate::help_functions::*;
use crate::localizer_core::generate_translation_hashmap;
use crate::notebook_enums::*;
use crate::notebook_info::NOTEBOOKS_INFO;
@ -211,42 +210,21 @@ fn hardlink_symlink(
for symhardlink_data in vec_symhardlink_data {
for file_to_symlink in symhardlink_data.files_to_symhardlink {
if let Err(e) = fs::remove_file(&file_to_symlink) {
add_text_to_text_view(
text_view_errors,
flg!(
"delete_file_failed",
generate_translation_hashmap(vec![("name", file_to_symlink.to_string()), ("reason", e.to_string())])
)
.as_str(),
);
add_text_to_text_view(text_view_errors, flg!("delete_file_failed", name = file_to_symlink, reason = e.to_string()).as_str());
continue;
};
#[cfg(target_family = "unix")]
{
if let Err(e) = std::os::unix::fs::symlink(&symhardlink_data.original_data, &file_to_symlink) {
add_text_to_text_view(
text_view_errors,
flg!(
"delete_file_failed",
generate_translation_hashmap(vec![("name", file_to_symlink.to_string()), ("reason", e.to_string())])
)
.as_str(),
);
add_text_to_text_view(text_view_errors, flg!("delete_file_failed", name = file_to_symlink, reason = e.to_string()).as_str());
continue;
};
}
#[cfg(target_family = "windows")]
{
if let Err(e) = std::os::windows::fs::symlink_file(&symhardlink_data.original_data, &file_to_symlink) {
add_text_to_text_view(
text_view_errors,
flg!(
"delete_file_failed",
generate_translation_hashmap(vec![("name", file_to_symlink.to_string()), ("reason", e.to_string())])
)
.as_str(),
);
add_text_to_text_view(text_view_errors, flg!("delete_file_failed", name = file_to_symlink, reason = e.to_string()).as_str());
continue;
};
}

View file

@ -7,7 +7,6 @@ use gtk4::{ResponseType, TreePath};
use crate::flg;
use crate::gui_structs::gui_data::GuiData;
use crate::help_functions::*;
use crate::localizer_core::generate_translation_hashmap;
use crate::notebook_enums::*;
use crate::notebook_info::NOTEBOOKS_INFO;
@ -54,14 +53,7 @@ pub fn connect_button_move(gui_data: &GuiData) {
}
if folders.len() != 1 {
add_text_to_text_view(
&text_view_errors,
flg!(
"move_files_choose_more_than_1_path",
generate_translation_hashmap(vec![("path_number", folders.len().to_string())])
)
.as_str(),
);
add_text_to_text_view(&text_view_errors, flg!("move_files_choose_more_than_1_path", path_number = folders.len()).as_str());
} else {
let folder = folders[0].clone();
if let Some(column_header) = nb_object.column_header {
@ -201,13 +193,13 @@ fn move_files_common(
let destination_file = destination_folder.join(file_name);
if Path::new(&thing).is_dir() {
if let Err(e) = fs_extra::dir::move_dir(&thing, &destination_file, &CopyOptions::new()) {
messages += flg!("move_folder_failed", generate_translation_hashmap(vec![("name", thing), ("reason", e.to_string())])).as_str();
messages += flg!("move_folder_failed", name = thing, reason = e.to_string()).as_str();
messages += "\n";
continue 'next_result;
}
} else {
if let Err(e) = fs_extra::file::move_file(&thing, &destination_file, &fs_extra::file::CopyOptions::new()) {
messages += flg!("move_file_failed", generate_translation_hashmap(vec![("name", thing), ("reason", e.to_string())])).as_str();
messages += flg!("move_file_failed", name = thing, reason = e.to_string()).as_str();
messages += "\n";
continue 'next_result;
@ -217,13 +209,7 @@ fn move_files_common(
moved_files += 1;
}
entry_info.set_text(
flg!(
"move_stats",
generate_translation_hashmap(vec![("num_files", moved_files.to_string()), ("all_files", selected_rows.len().to_string())])
)
.as_str(),
);
entry_info.set_text(flg!("move_stats", num_files = moved_files, all_files = selected_rows.len()).as_str());
text_view_errors.buffer().set_text(messages.as_str());
}

View file

@ -11,7 +11,6 @@ use czkawka_core::common_traits::PrintResults;
use crate::flg;
use crate::gui_structs::gui_data::GuiData;
use crate::help_functions::BottomButtonsEnum;
use crate::localizer_core::generate_translation_hashmap;
use crate::notebook_enums::*;
pub fn connect_button_save(gui_data: &GuiData) {
@ -76,7 +75,7 @@ fn post_save_things(
buttons_save: &Button,
current_path: String,
) {
entry_info.set_text(&flg!("save_results_to_file", generate_translation_hashmap(vec![("name", current_path),])));
entry_info.set_text(&flg!("save_results_to_file", name = current_path));
// Set state
{
buttons_save.hide();

View file

@ -5,7 +5,6 @@ use std::thread;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use glib::Sender as glibSender;
use gtk4::prelude::*;
use gtk4::Grid;
@ -35,10 +34,10 @@ use crate::taskbar_progress::tbp_flags::TBPF_NOPROGRESS;
use crate::{flg, DEFAULT_MAXIMAL_FILE_SIZE, DEFAULT_MINIMAL_CACHE_SIZE, DEFAULT_MINIMAL_FILE_SIZE};
#[allow(clippy::too_many_arguments)]
pub fn connect_button_search(gui_data: &GuiData, glib_stop_sender: glibSender<Message>, progress_sender: Sender<ProgressData>) {
pub fn connect_button_search(gui_data: &GuiData, result_sender: Sender<Message>, progress_sender: Sender<ProgressData>) {
let buttons_array = gui_data.bottom_buttons.buttons_array.clone();
let buttons_search_clone = gui_data.bottom_buttons.buttons_search.clone();
let grid_progress_stages = gui_data.progress_window.grid_progress_stages.clone();
let grid_progress = gui_data.progress_window.grid_progress.clone();
let label_stage = gui_data.progress_window.label_stage.clone();
let notebook_main = gui_data.main_notebook.notebook_main.clone();
let notebook_upper = gui_data.upper_notebook.notebook_upper.clone();
@ -55,7 +54,7 @@ pub fn connect_button_search(gui_data: &GuiData, glib_stop_sender: glibSender<Me
let gui_data = gui_data.clone();
buttons_search_clone.connect_clicked(move |_| {
let loaded_common_items = LoadedCommonItems::load_items(&gui_data);
let loaded_commons = LoadedCommonItems::load_items(&gui_data);
// Check if user selected all referenced folders
let list_store_included_directories = get_list_store(&tree_view_included_directories);
@ -83,104 +82,27 @@ pub fn connect_button_search(gui_data: &GuiData, glib_stop_sender: glibSender<Me
reset_text_view(&text_view_errors);
let glib_stop_sender = glib_stop_sender.clone();
let result_sender = result_sender.clone();
let stop_receiver = stop_receiver.clone();
// Consume any stale stop messages.
stop_receiver.try_iter().for_each(|()| ());
label_stage.show();
let progress_sender = progress_sender.clone();
match to_notebook_main_enum(notebook_main.current_page().unwrap()) {
NotebookMainEnum::Duplicate => duplicate_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
),
NotebookMainEnum::EmptyFiles => empty_files_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
),
NotebookMainEnum::EmptyDirectories => empty_directories_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
),
NotebookMainEnum::BigFiles => big_files_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
),
NotebookMainEnum::Temporary => temporary_files_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
),
NotebookMainEnum::SimilarImages => similar_image_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
),
NotebookMainEnum::SimilarVideos => similar_video_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
),
NotebookMainEnum::SameMusic => same_music_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
&show_dialog,
),
NotebookMainEnum::Symlinks => bad_symlinks_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
),
NotebookMainEnum::BrokenFiles => broken_files_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
&show_dialog,
),
NotebookMainEnum::BadExtensions => bad_extensions_search(
&gui_data,
loaded_common_items,
stop_receiver,
glib_stop_sender,
&grid_progress_stages,
progress_sender.clone(),
),
NotebookMainEnum::Duplicate => duplicate_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender),
NotebookMainEnum::EmptyFiles => empty_files_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender),
NotebookMainEnum::EmptyDirectories => empty_dirs_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender),
NotebookMainEnum::BigFiles => big_files_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender),
NotebookMainEnum::Temporary => temporary_files_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender),
NotebookMainEnum::SimilarImages => similar_image_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender),
NotebookMainEnum::SimilarVideos => similar_video_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender),
NotebookMainEnum::SameMusic => same_music_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender, &show_dialog),
NotebookMainEnum::Symlinks => bad_symlinks_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender),
NotebookMainEnum::BrokenFiles => broken_files_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender, &show_dialog),
NotebookMainEnum::BadExtensions => bad_extensions_search(&gui_data, loaded_commons, stop_receiver, result_sender, &grid_progress, progress_sender),
}
window_progress.set_default_size(1, 1);
@ -201,6 +123,7 @@ struct LoadedCommonItems {
recursive_search: bool,
excluded_items: Vec<String>,
allowed_extensions: String,
excluded_extensions: String,
hide_hard_links: bool,
use_cache: bool,
save_also_as_json: bool,
@ -217,6 +140,7 @@ impl LoadedCommonItems {
let check_button_settings_hide_hard_links = gui_data.settings.check_button_settings_hide_hard_links.clone();
let check_button_settings_use_cache = gui_data.settings.check_button_settings_use_cache.clone();
let entry_allowed_extensions = gui_data.upper_notebook.entry_allowed_extensions.clone();
let entry_excluded_extensions = gui_data.upper_notebook.entry_excluded_extensions.clone();
let entry_excluded_items = gui_data.upper_notebook.entry_excluded_items.clone();
let entry_general_maximal_size = gui_data.upper_notebook.entry_general_maximal_size.clone();
let entry_general_minimal_size = gui_data.upper_notebook.entry_general_minimal_size.clone();
@ -241,6 +165,7 @@ impl LoadedCommonItems {
.map(std::string::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();
let hide_hard_links = check_button_settings_hide_hard_links.is_active();
let use_cache = check_button_settings_use_cache.is_active();
let save_also_as_json = check_button_settings_save_also_json.is_active();
@ -269,6 +194,7 @@ impl LoadedCommonItems {
recursive_search,
excluded_items,
allowed_extensions,
excluded_extensions,
hide_hard_links,
use_cache,
save_also_as_json,
@ -282,13 +208,13 @@ impl LoadedCommonItems {
fn duplicate_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.show();
grid_progress.show();
let combo_box_duplicate_check_method = gui_data.main_notebook.combo_box_duplicate_check_method.clone();
let combo_box_duplicate_hash_type = gui_data.main_notebook.combo_box_duplicate_hash_type.clone();
@ -319,41 +245,32 @@ fn duplicate_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut df = DuplicateFinder::new();
df.set_included_directory(loaded_common_items.included_directories);
df.set_excluded_directory(loaded_common_items.excluded_directories);
df.set_reference_directory(loaded_common_items.reference_directories);
df.set_recursive_search(loaded_common_items.recursive_search);
df.set_excluded_items(loaded_common_items.excluded_items);
df.set_allowed_extensions(loaded_common_items.allowed_extensions);
df.set_minimal_file_size(loaded_common_items.minimal_file_size);
df.set_maximal_file_size(loaded_common_items.maximal_file_size);
df.set_minimal_cache_file_size(loaded_common_items.minimal_cache_file_size);
df.set_minimal_prehash_cache_file_size(minimal_prehash_cache_file_size);
df.set_check_method(check_method);
df.set_hash_type(hash_type);
df.set_save_also_as_json(loaded_common_items.save_also_as_json);
df.set_ignore_hard_links(loaded_common_items.hide_hard_links);
df.set_use_cache(loaded_common_items.use_cache);
df.set_use_prehash_cache(use_prehash_cache);
df.set_delete_outdated_cache(delete_outdated_cache);
df.set_case_sensitive_name_comparison(case_sensitive_name_comparison);
df.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
df.find_duplicates(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::Duplicates(df)).unwrap();
let mut item = DuplicateFinder::new();
set_common_settings(&mut item, &loaded_commons);
item.set_minimal_cache_file_size(loaded_commons.minimal_cache_file_size);
item.set_minimal_prehash_cache_file_size(minimal_prehash_cache_file_size);
item.set_check_method(check_method);
item.set_hash_type(hash_type);
item.set_ignore_hard_links(loaded_commons.hide_hard_links);
item.set_use_prehash_cache(use_prehash_cache);
item.set_delete_outdated_cache(delete_outdated_cache);
item.set_case_sensitive_name_comparison(case_sensitive_name_comparison);
item.find_duplicates(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::Duplicates(item)).unwrap();
})
.unwrap();
}
fn empty_files_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.hide();
grid_progress.hide();
let tree_view_empty_files_finder = gui_data.main_notebook.tree_view_empty_files_finder.clone();
clean_tree_view(&tree_view_empty_files_finder);
@ -361,29 +278,24 @@ fn empty_files_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut vf = EmptyFiles::new();
let mut item = EmptyFiles::new();
vf.set_included_directory(loaded_common_items.included_directories);
vf.set_excluded_directory(loaded_common_items.excluded_directories);
vf.set_recursive_search(loaded_common_items.recursive_search);
vf.set_excluded_items(loaded_common_items.excluded_items);
vf.set_allowed_extensions(loaded_common_items.allowed_extensions);
vf.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
vf.find_empty_files(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::EmptyFiles(vf)).unwrap();
set_common_settings(&mut item, &loaded_commons);
item.find_empty_files(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::EmptyFiles(item)).unwrap();
})
.unwrap();
}
fn empty_directories_search(
fn empty_dirs_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.hide();
grid_progress.hide();
let tree_view_empty_folder_finder = gui_data.main_notebook.tree_view_empty_folder_finder.clone();
clean_tree_view(&tree_view_empty_folder_finder);
@ -391,26 +303,24 @@ fn empty_directories_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut ef = EmptyFolder::new();
ef.set_included_directory(loaded_common_items.included_directories);
ef.set_excluded_directory(loaded_common_items.excluded_directories);
ef.set_excluded_items(loaded_common_items.excluded_items);
ef.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
ef.find_empty_folders(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::EmptyFolders(ef)).unwrap();
let mut item = EmptyFolder::new();
set_common_settings(&mut item, &loaded_commons);
item.find_empty_folders(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::EmptyFolders(item)).unwrap();
})
.unwrap();
}
fn big_files_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.hide();
grid_progress.hide();
let combo_box_big_files_mode = gui_data.main_notebook.combo_box_big_files_mode.clone();
let entry_big_files_number = gui_data.main_notebook.entry_big_files_number.clone();
@ -425,31 +335,26 @@ fn big_files_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut bf = BigFile::new();
let mut item = BigFile::new();
bf.set_included_directory(loaded_common_items.included_directories);
bf.set_excluded_directory(loaded_common_items.excluded_directories);
bf.set_recursive_search(loaded_common_items.recursive_search);
bf.set_excluded_items(loaded_common_items.excluded_items);
bf.set_allowed_extensions(loaded_common_items.allowed_extensions);
bf.set_number_of_files_to_check(numbers_of_files_to_check);
bf.set_search_mode(big_files_mode);
bf.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
bf.find_big_files(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::BigFiles(bf)).unwrap();
set_common_settings(&mut item, &loaded_commons);
item.set_number_of_files_to_check(numbers_of_files_to_check);
item.set_search_mode(big_files_mode);
item.find_big_files(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::BigFiles(item)).unwrap();
})
.unwrap();
}
fn temporary_files_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.hide();
grid_progress.hide();
let tree_view_temporary_files_finder = gui_data.main_notebook.tree_view_temporary_files_finder.clone();
clean_tree_view(&tree_view_temporary_files_finder);
@ -457,29 +362,25 @@ fn temporary_files_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut tf = Temporary::new();
let mut item = Temporary::new();
tf.set_included_directory(loaded_common_items.included_directories);
tf.set_excluded_directory(loaded_common_items.excluded_directories);
tf.set_recursive_search(loaded_common_items.recursive_search);
tf.set_excluded_items(loaded_common_items.excluded_items);
tf.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
tf.find_temporary_files(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::Temporary(tf)).unwrap();
set_common_settings(&mut item, &loaded_commons);
item.find_temporary_files(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::Temporary(item)).unwrap();
})
.unwrap();
}
fn same_music_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
show_dialog: &Arc<AtomicBool>,
) {
grid_progress_stages.show();
grid_progress.show();
let check_button_music_artist: gtk4::CheckButton = gui_data.main_notebook.check_button_music_artist.clone();
let check_button_music_title: gtk4::CheckButton = gui_data.main_notebook.check_button_music_title.clone();
@ -528,26 +429,16 @@ fn same_music_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut mf = SameMusic::new();
let mut item = SameMusic::new();
mf.set_included_directory(loaded_common_items.included_directories);
mf.set_excluded_directory(loaded_common_items.excluded_directories);
mf.set_reference_directory(loaded_common_items.reference_directories);
mf.set_excluded_items(loaded_common_items.excluded_items);
mf.set_use_cache(loaded_common_items.use_cache);
mf.set_minimal_file_size(loaded_common_items.minimal_file_size);
mf.set_maximal_file_size(loaded_common_items.maximal_file_size);
mf.set_allowed_extensions(loaded_common_items.allowed_extensions);
mf.set_recursive_search(loaded_common_items.recursive_search);
mf.set_music_similarity(music_similarity);
mf.set_maximum_difference(maximum_difference);
mf.set_minimum_segment_duration(minimum_segment_duration);
mf.set_check_type(check_method);
mf.set_approximate_comparison(approximate_comparison);
mf.set_save_also_as_json(loaded_common_items.save_also_as_json);
mf.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
mf.find_same_music(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::SameMusic(mf)).unwrap();
set_common_settings(&mut item, &loaded_commons);
item.set_music_similarity(music_similarity);
item.set_maximum_difference(maximum_difference);
item.set_minimum_segment_duration(minimum_segment_duration);
item.set_check_type(check_method);
item.set_approximate_comparison(approximate_comparison);
item.find_same_music(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::SameMusic(item)).unwrap();
})
.unwrap();
} else {
@ -577,14 +468,14 @@ fn same_music_search(
fn broken_files_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
show_dialog: &Arc<AtomicBool>,
) {
grid_progress_stages.show();
grid_progress.show();
let check_button_broken_files_archive: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_archive.clone();
let check_button_broken_files_pdf: gtk4::CheckButton = gui_data.main_notebook.check_button_broken_files_pdf.clone();
@ -613,19 +504,12 @@ fn broken_files_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut br = BrokenFiles::new();
let mut item = BrokenFiles::new();
br.set_included_directory(loaded_common_items.included_directories);
br.set_excluded_directory(loaded_common_items.excluded_directories);
br.set_recursive_search(loaded_common_items.recursive_search);
br.set_excluded_items(loaded_common_items.excluded_items);
br.set_use_cache(loaded_common_items.use_cache);
br.set_allowed_extensions(loaded_common_items.allowed_extensions);
br.set_save_also_as_json(loaded_common_items.save_also_as_json);
br.set_checked_types(checked_types);
br.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
br.find_broken_files(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::BrokenFiles(br)).unwrap();
set_common_settings(&mut item, &loaded_commons);
item.set_checked_types(checked_types);
item.find_broken_files(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::BrokenFiles(item)).unwrap();
})
.unwrap();
} else {
@ -655,13 +539,13 @@ fn broken_files_search(
fn similar_image_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.show();
grid_progress.show();
let combo_box_image_hash_size = gui_data.main_notebook.combo_box_image_hash_size.clone();
let combo_box_image_hash_algorithm = gui_data.main_notebook.combo_box_image_hash_algorithm.clone();
@ -693,40 +577,30 @@ fn similar_image_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut sf = SimilarImages::new();
let mut item = SimilarImages::new();
sf.set_included_directory(loaded_common_items.included_directories);
sf.set_excluded_directory(loaded_common_items.excluded_directories);
sf.set_reference_directory(loaded_common_items.reference_directories);
sf.set_recursive_search(loaded_common_items.recursive_search);
sf.set_excluded_items(loaded_common_items.excluded_items);
sf.set_minimal_file_size(loaded_common_items.minimal_file_size);
sf.set_maximal_file_size(loaded_common_items.maximal_file_size);
sf.set_similarity(similarity);
sf.set_use_cache(loaded_common_items.use_cache);
sf.set_hash_alg(hash_alg);
sf.set_hash_size(hash_size);
sf.set_image_filter(image_filter);
sf.set_allowed_extensions(loaded_common_items.allowed_extensions);
sf.set_delete_outdated_cache(delete_outdated_cache);
sf.set_exclude_images_with_same_size(ignore_same_size);
sf.set_save_also_as_json(loaded_common_items.save_also_as_json);
sf.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
sf.find_similar_images(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::SimilarImages(sf)).unwrap();
set_common_settings(&mut item, &loaded_commons);
item.set_similarity(similarity);
item.set_hash_alg(hash_alg);
item.set_hash_size(hash_size);
item.set_image_filter(image_filter);
item.set_delete_outdated_cache(delete_outdated_cache);
item.set_exclude_images_with_same_size(ignore_same_size);
item.find_similar_images(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::SimilarImages(item)).unwrap();
})
.unwrap();
}
fn similar_video_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.show();
grid_progress.show();
let check_button_video_ignore_same_size = gui_data.main_notebook.check_button_video_ignore_same_size.clone();
let check_button_settings_similar_videos_delete_outdated_cache = gui_data.settings.check_button_settings_similar_videos_delete_outdated_cache.clone();
@ -743,37 +617,27 @@ fn similar_video_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut sf = SimilarVideos::new();
let mut item = SimilarVideos::new();
sf.set_included_directory(loaded_common_items.included_directories);
sf.set_excluded_directory(loaded_common_items.excluded_directories);
sf.set_reference_directory(loaded_common_items.reference_directories);
sf.set_recursive_search(loaded_common_items.recursive_search);
sf.set_excluded_items(loaded_common_items.excluded_items);
sf.set_minimal_file_size(loaded_common_items.minimal_file_size);
sf.set_maximal_file_size(loaded_common_items.maximal_file_size);
sf.set_allowed_extensions(loaded_common_items.allowed_extensions);
sf.set_use_cache(loaded_common_items.use_cache);
sf.set_tolerance(tolerance);
sf.set_delete_outdated_cache(delete_outdated_cache);
sf.set_exclude_videos_with_same_size(ignore_same_size);
sf.set_save_also_as_json(loaded_common_items.save_also_as_json);
sf.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
sf.find_similar_videos(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::SimilarVideos(sf)).unwrap();
set_common_settings(&mut item, &loaded_commons);
item.set_tolerance(tolerance);
item.set_delete_outdated_cache(delete_outdated_cache);
item.set_exclude_videos_with_same_size(ignore_same_size);
item.find_similar_videos(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::SimilarVideos(item)).unwrap();
})
.unwrap();
}
fn bad_symlinks_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.hide();
grid_progress.hide();
let tree_view_invalid_symlinks = gui_data.main_notebook.tree_view_invalid_symlinks.clone();
clean_tree_view(&tree_view_invalid_symlinks);
@ -781,29 +645,24 @@ fn bad_symlinks_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut isf = InvalidSymlinks::new();
let mut item = InvalidSymlinks::new();
isf.set_included_directory(loaded_common_items.included_directories);
isf.set_excluded_directory(loaded_common_items.excluded_directories);
isf.set_recursive_search(loaded_common_items.recursive_search);
isf.set_excluded_items(loaded_common_items.excluded_items);
isf.set_allowed_extensions(loaded_common_items.allowed_extensions);
isf.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
isf.find_invalid_links(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::InvalidSymlinks(isf)).unwrap();
set_common_settings(&mut item, &loaded_commons);
item.find_invalid_links(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::InvalidSymlinks(item)).unwrap();
})
.unwrap();
}
fn bad_extensions_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
loaded_commons: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
result_sender: Sender<Message>,
grid_progress: &Grid,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.show();
grid_progress.show();
let tree_view_bad_extensions = gui_data.main_notebook.tree_view_bad_extensions.clone();
clean_tree_view(&tree_view_bad_extensions);
@ -811,22 +670,33 @@ fn bad_extensions_search(
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut be = BadExtensions::new();
let mut item = BadExtensions::new();
be.set_included_directory(loaded_common_items.included_directories);
be.set_excluded_directory(loaded_common_items.excluded_directories);
be.set_excluded_items(loaded_common_items.excluded_items);
be.set_minimal_file_size(loaded_common_items.minimal_file_size);
be.set_maximal_file_size(loaded_common_items.maximal_file_size);
be.set_allowed_extensions(loaded_common_items.allowed_extensions);
be.set_recursive_search(loaded_common_items.recursive_search);
be.set_exclude_other_filesystems(loaded_common_items.ignore_other_filesystems);
be.find_bad_extensions_files(Some(&stop_receiver), Some(&progress_data_sender));
glib_stop_sender.send(Message::BadExtensions(be)).unwrap();
set_common_settings(&mut item, &loaded_commons);
item.find_bad_extensions_files(Some(&stop_receiver), Some(&progress_data_sender));
result_sender.send(Message::BadExtensions(item)).unwrap();
})
.unwrap();
}
fn set_common_settings<T>(component: &mut T, loaded_commons: &LoadedCommonItems)
where
T: CommonData,
{
component.set_included_directory(loaded_commons.included_directories.clone());
component.set_excluded_directory(loaded_commons.excluded_directories.clone());
component.set_reference_directory(loaded_commons.reference_directories.clone());
component.set_recursive_search(loaded_commons.recursive_search);
component.set_allowed_extensions(loaded_commons.allowed_extensions.clone());
component.set_excluded_extensions(loaded_commons.excluded_extensions.clone());
component.set_excluded_items(loaded_commons.excluded_items.clone());
component.set_exclude_other_filesystems(loaded_commons.ignore_other_filesystems);
component.set_use_cache(loaded_commons.use_cache);
component.set_save_also_as_json(loaded_commons.save_also_as_json);
component.set_minimal_file_size(loaded_commons.minimal_file_size);
component.set_maximal_file_size(loaded_commons.maximal_file_size);
}
#[fun_time(message = "clean_tree_view", level = "debug")]
fn clean_tree_view(tree_view: &gtk4::TreeView) {
let list_store = get_list_store(tree_view);

View file

@ -1,9 +1,10 @@
use czkawka_core::common::regex_check;
use czkawka_core::common_items::new_excluded_item;
use gtk4::prelude::*;
use gtk4::{ResponseType, TreeIter, Window};
use regex::Regex;
use czkawka_core::common::regex_check;
use czkawka_core::common_items::new_excluded_item;
use crate::flg;
use crate::gui_structs::gui_data::GuiData;
use crate::help_functions::*;

View file

@ -1,10 +1,9 @@
use crossbeam_channel::Receiver;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::Duration;
use crossbeam_channel::Receiver;
use glib::MainContext;
use gtk4::prelude::*;
use gtk4::ProgressBar;
@ -164,10 +163,7 @@ fn progress_collect_items(gui_data: &GuiData, item: &ProgressData, files: bool)
if files {
label_stage.set_text(&flg!("progress_scanning_general_file", file_number_tm(item)));
} else {
label_stage.set_text(&flg!(
"progress_scanning_empty_folders",
generate_translation_hashmap(vec![("folder_number", item.entries_checked.to_string())])
));
label_stage.set_text(&flg!("progress_scanning_empty_folders", folder_number = item.entries_checked));
}
}

View file

@ -1,7 +1,7 @@
use gdk4::{DragAction, FileList};
use std::collections::HashSet;
use std::path::PathBuf;
use gdk4::{DragAction, FileList};
use gtk4::prelude::*;
use gtk4::{DropTarget, FileChooserNative, Notebook, Orientation, ResponseType, TreeView, Window};

View file

@ -1,16 +1,16 @@
use std::collections::BTreeMap;
use std::default::Default;
use czkawka_core::common_cache::{
get_duplicate_cache_file, get_similar_images_cache_file, get_similar_videos_cache_file, load_cache_from_file_generalized_by_path, load_cache_from_file_generalized_by_size,
save_cache_to_file_generalized,
};
use directories_next::ProjectDirs;
use gtk4::prelude::*;
use gtk4::{Label, ResponseType, Window};
use image::imageops::FilterType;
use image_hasher::HashAlg;
use czkawka_core::common_cache::{
get_duplicate_cache_file, get_similar_images_cache_file, get_similar_videos_cache_file, load_cache_from_file_generalized_by_path, load_cache_from_file_generalized_by_size,
save_cache_to_file_generalized,
};
use czkawka_core::common_messages::Messages;
use czkawka_core::duplicate::HashType;

View file

@ -108,7 +108,7 @@ impl GuiAbout {
}
pub fn update_language(&self) {
let mut comment_text: String = "2020 - 2023 Rafał Mikrut(qarmin)\n\n".to_string();
let mut comment_text: String = "2020 - 2024 Rafał Mikrut(qarmin)\n\n".to_string();
comment_text += &flg!("about_window_motto");
self.about_dialog.set_comments(Some(&comment_text));

View file

@ -15,7 +15,7 @@ pub struct GuiProgressDialog {
pub label_progress_current_stage: gtk4::Label,
pub label_progress_all_stages: gtk4::Label,
pub grid_progress_stages: gtk4::Grid,
pub grid_progress: gtk4::Grid,
pub button_stop_in_dialog: gtk4::Button,
pub evk_button_stop_in_dialog: EventControllerKey,
@ -38,7 +38,7 @@ impl GuiProgressDialog {
let label_progress_current_stage: gtk4::Label = builder.object("label_progress_current_stage").unwrap();
let label_progress_all_stages: gtk4::Label = builder.object("label_progress_all_stages").unwrap();
let grid_progress_stages: gtk4::Grid = builder.object("grid_progress_stages").unwrap();
let grid_progress: gtk4::Grid = builder.object("grid_progress").unwrap();
let button_stop_in_dialog: gtk4::Button = builder.object("button_stop_in_dialog").unwrap();
let evk_button_stop_in_dialog = EventControllerKey::new();
@ -53,7 +53,7 @@ impl GuiProgressDialog {
label_stage,
label_progress_current_stage,
label_progress_all_stages,
grid_progress_stages,
grid_progress,
button_stop_in_dialog,
evk_button_stop_in_dialog,
}

View file

@ -23,6 +23,7 @@ pub struct GuiUpperNotebook {
pub entry_excluded_items: gtk4::Entry,
pub entry_allowed_extensions: gtk4::Entry,
pub entry_excluded_extensions: gtk4::Entry,
pub check_button_recursive: gtk4::CheckButton,
@ -35,6 +36,7 @@ pub struct GuiUpperNotebook {
pub label_excluded_items: gtk4::Label,
pub label_allowed_extensions: gtk4::Label,
pub label_excluded_extensions: gtk4::Label,
pub entry_general_minimal_size: gtk4::Entry,
pub entry_general_maximal_size: gtk4::Entry,
@ -64,6 +66,7 @@ impl GuiUpperNotebook {
tree_view_excluded_directories.add_controller(gc_tree_view_excluded_directories.clone());
let entry_allowed_extensions: gtk4::Entry = builder.object("entry_allowed_extensions").unwrap();
let entry_excluded_extensions: gtk4::Entry = builder.object("entry_excluded_extensions").unwrap();
let entry_excluded_items: gtk4::Entry = builder.object("entry_excluded_items").unwrap();
let check_button_recursive: gtk4::CheckButton = builder.object("check_button_recursive").unwrap();
@ -77,6 +80,7 @@ impl GuiUpperNotebook {
let label_excluded_items: gtk4::Label = builder.object("label_excluded_items").unwrap();
let label_allowed_extensions: gtk4::Label = builder.object("label_allowed_extensions").unwrap();
let label_excluded_extensions: gtk4::Label = builder.object("label_excluded_extensions").unwrap();
let entry_general_minimal_size: gtk4::Entry = builder.object("entry_general_minimal_size").unwrap();
let entry_general_maximal_size: gtk4::Entry = builder.object("entry_general_maximal_size").unwrap();
@ -103,6 +107,7 @@ impl GuiUpperNotebook {
gc_tree_view_excluded_directories,
entry_excluded_items,
entry_allowed_extensions,
entry_excluded_extensions,
check_button_recursive,
buttons_manual_add_included_directory,
buttons_add_included_directory,
@ -112,6 +117,7 @@ impl GuiUpperNotebook {
buttons_remove_excluded_directory,
label_excluded_items,
label_allowed_extensions,
label_excluded_extensions,
entry_general_minimal_size,
entry_general_maximal_size,
label_general_size_bytes,
@ -141,11 +147,14 @@ impl GuiUpperNotebook {
self.label_allowed_extensions.set_tooltip_text(Some(&flg!("upper_allowed_extensions_tooltip")));
self.entry_allowed_extensions.set_tooltip_text(Some(&flg!("upper_allowed_extensions_tooltip")));
self.label_excluded_extensions.set_tooltip_text(Some(&flg!("upper_excluded_extensions_tooltip")));
self.entry_excluded_extensions.set_tooltip_text(Some(&flg!("upper_excluded_extensions_tooltip")));
self.label_excluded_items.set_tooltip_text(Some(&flg!("upper_excluded_items_tooltip")));
self.entry_excluded_items.set_tooltip_text(Some(&flg!("upper_excluded_items_tooltip")));
self.label_excluded_items.set_label(&flg!("upper_excluded_items"));
self.label_allowed_extensions.set_label(&flg!("upper_allowed_extensions"));
self.label_excluded_extensions.set_label(&flg!("upper_excluded_extensions"));
self.label_general_size_bytes.set_label(&flg!("main_label_size_bytes"));
self.label_general_min_size.set_label(&flg!("main_label_min_size"));

View file

@ -1,7 +1,7 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use std::io::BufReader;
use std::path::PathBuf;
use std::path::{PathBuf, MAIN_SEPARATOR};
use gdk4::gdk_pixbuf::{InterpType, Pixbuf};
use glib::Error;
@ -14,7 +14,6 @@ use once_cell::sync::OnceCell;
use czkawka_core::bad_extensions::BadExtensions;
use czkawka_core::big_file::BigFile;
use czkawka_core::broken_files::BrokenFiles;
use czkawka_core::common::CHARACTER;
use czkawka_core::common_dir_traversal;
use czkawka_core::common_messages::Messages;
use czkawka_core::duplicate::DuplicateFinder;
@ -414,7 +413,7 @@ pub fn get_notebook_object_from_tree_view(tree_view: &TreeView) -> &NotebookObje
pub fn get_full_name_from_path_name(path: &str, name: &str) -> String {
let mut string = String::with_capacity(path.len() + name.len() + 1);
string.push_str(path);
string.push(CHARACTER);
string.push(MAIN_SEPARATOR);
string.push_str(name);
string
}

View file

@ -510,14 +510,7 @@ fn show_preview(
let image = match get_dynamic_image_from_heic(file_name) {
Ok(t) => t,
Err(e) => {
add_text_to_text_view(
text_view_errors,
flg!(
"preview_image_opening_failure",
generate_translation_hashmap(vec![("name", file_name.to_string()), ("reason", e.to_string())])
)
.as_str(),
);
add_text_to_text_view(text_view_errors, flg!("preview_image_opening_failure", name = file_name, reason = e.to_string()).as_str());
break 'dir;
}
};
@ -526,14 +519,7 @@ fn show_preview(
match get_pixbuf_from_dynamic_image(&image) {
Ok(t) => t,
Err(e) => {
add_text_to_text_view(
text_view_errors,
flg!(
"preview_image_opening_failure",
generate_translation_hashmap(vec![("name", file_name.to_string()), ("reason", e.to_string())])
)
.as_str(),
);
add_text_to_text_view(text_view_errors, flg!("preview_image_opening_failure", name = file_name, reason = e.to_string()).as_str());
break 'dir;
}
}
@ -559,10 +545,7 @@ fn show_preview(
pixbuf = match resize_pixbuf_dimension(&pixbuf, (800, 800), InterpType::Bilinear) {
None => {
add_text_to_text_view(
text_view_errors,
flg!("preview_image_resize_failure", generate_translation_hashmap(vec![("name", file_name.to_string())])).as_str(),
);
add_text_to_text_view(text_view_errors, flg!("preview_image_resize_failure", name = file_name).as_str());
break 'dir;
}
Some(pixbuf) => pixbuf,

View file

@ -5,11 +5,10 @@
#![allow(clippy::type_complexity)]
#![allow(clippy::needless_late_init)]
use crossbeam_channel::{unbounded, Receiver, Sender};
use std::env;
use std::ffi::OsString;
use glib::Priority;
use crossbeam_channel::{unbounded, Receiver, Sender};
use gtk4::gio::ApplicationFlags;
use gtk4::prelude::*;
use gtk4::Application;
@ -82,10 +81,7 @@ fn main() {
fn build_ui(application: &Application, arguments: &[OsString]) {
let gui_data: GuiData = GuiData::new_with_application(application);
// Used for getting data from thread
// TODO - deprecation happened without any example, so not sure how new code should look like
#[allow(deprecated)]
let (glib_stop_sender, glib_stop_receiver) = glib::MainContext::channel(Priority::default());
let (result_sender, result_receiver) = unbounded();
// Futures progress report
let (progress_sender, progress_receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();
@ -111,7 +107,7 @@ fn build_ui(application: &Application, arguments: &[OsString]) {
connect_button_delete(&gui_data);
connect_button_save(&gui_data);
connect_button_search(&gui_data, glib_stop_sender, progress_sender);
connect_button_search(&gui_data, result_sender, progress_sender);
connect_button_select(&gui_data);
connect_button_sort(&gui_data);
connect_button_stop(&gui_data);
@ -124,7 +120,7 @@ fn build_ui(application: &Application, arguments: &[OsString]) {
connect_selection_of_directories(&gui_data);
connect_popover_select(&gui_data);
connect_popover_sort(&gui_data);
connect_compute_results(&gui_data, glib_stop_receiver);
connect_compute_results(&gui_data, result_receiver);
connect_progress_window(&gui_data, progress_receiver);
connect_show_hide_ui(&gui_data);
connect_settings(&gui_data);

View file

@ -9,7 +9,7 @@ use directories_next::ProjectDirs;
use gtk4::prelude::*;
use gtk4::{ComboBoxText, ScrolledWindow, TextView, TreeView};
use czkawka_core::common::get_default_number_of_threads;
use czkawka_core::common::get_all_available_threads;
use czkawka_core::common_dir_traversal::CheckingMethod;
use czkawka_core::common_items::DEFAULT_EXCLUDED_ITEMS;
use czkawka_core::similar_images::SIMILAR_VALUES;
@ -21,7 +21,6 @@ use crate::gui_structs::gui_upper_notebook::GuiUpperNotebook;
use crate::help_combo_box::DUPLICATES_CHECK_METHOD_COMBO_BOX;
use crate::help_functions::*;
use crate::language_functions::{get_language_from_combo_box_text, LANGUAGES_ALL};
use crate::localizer_core::generate_translation_hashmap;
const SAVE_FILE_NAME: &str = "czkawka_gui_config_4.txt";
@ -82,7 +81,7 @@ impl LoadSaveStruct {
pub fn get_vector_string(&self, key: &str, default_value: Vec<String>) -> Vec<String> {
if self.loaded_items.contains_key(key) {
let mut new_vector = Vec::new();
for i in self.loaded_items.get(key).unwrap() {
for i in &self.loaded_items[key] {
if !i.trim().is_empty() {
new_vector.push(i.trim().to_string());
}
@ -105,19 +104,13 @@ impl LoadSaveStruct {
}
pub fn get_string(&self, key: String, default_value: String) -> String {
if self.loaded_items.contains_key(&key) {
let item = self.loaded_items.get(&key).unwrap().clone().into_iter().filter(|e| !e.is_empty()).collect::<Vec<String>>();
let item = &self.loaded_items[&key].clone().into_iter().filter(|e| !e.is_empty()).collect::<Vec<String>>();
return if item.len() == 1 {
item[0].clone()
} else if item.is_empty() {
String::new()
} else {
add_text_to_text_view(
&self.text_view,
&flg!(
"saving_loading_invalid_string",
generate_translation_hashmap(vec![("key", key), ("result", format!("{item:?}"))])
),
);
add_text_to_text_view(&self.text_view, &flg!("saving_loading_invalid_string", key = key, result = format!("{item:?}")));
default_value
};
}
@ -126,7 +119,7 @@ impl LoadSaveStruct {
}
pub fn get_object<T: std::str::FromStr>(&self, key: String, default_value: T) -> T {
if self.loaded_items.contains_key(&key) {
let item = self.loaded_items.get(&key).unwrap().clone().into_iter().filter(|e| !e.is_empty()).collect::<Vec<String>>();
let item = &self.loaded_items[&key].clone().into_iter().filter(|e| !e.is_empty()).collect::<Vec<String>>();
return if item.len() == 1 {
if let Ok(t) = item[0].parse::<T>() {
@ -136,13 +129,7 @@ impl LoadSaveStruct {
default_value
}
} else {
add_text_to_text_view(
&self.text_view,
&flg!(
"saving_loading_invalid_int",
generate_translation_hashmap(vec![("key", key), ("result", format!("{item:?}"))])
),
);
add_text_to_text_view(&self.text_view, &flg!("saving_loading_invalid_int", key = key, result = format!("{item:?}")));
default_value
};
}
@ -151,7 +138,7 @@ impl LoadSaveStruct {
}
pub fn get_bool(&self, key: String, default_value: bool) -> bool {
if self.loaded_items.contains_key(&key) {
let item = self.loaded_items.get(&key).unwrap().clone().into_iter().filter(|e| !e.is_empty()).collect::<Vec<String>>();
let item = &self.loaded_items[&key].clone().into_iter().filter(|e| !e.is_empty()).collect::<Vec<String>>();
return if item.len() == 1 {
let text = item[0].trim().to_lowercase();
if text == "false" || text == "0" {
@ -159,23 +146,11 @@ impl LoadSaveStruct {
} else if text == "true" || text == "1" {
true
} else {
add_text_to_text_view(
&self.text_view,
&flg!(
"saving_loading_decode_problem_bool",
generate_translation_hashmap(vec![("key", key), ("result", item[0].to_string())])
),
);
add_text_to_text_view(&self.text_view, &flg!("saving_loading_decode_problem_bool", key = key, result = item[0].to_string()));
default_value
}
} else {
add_text_to_text_view(
&self.text_view,
&flg!(
"saving_loading_invalid_bool",
generate_translation_hashmap(vec![("key", key), ("result", format!("{item:?}"))])
),
);
add_text_to_text_view(&self.text_view, &flg!("saving_loading_invalid_bool", key = key, result = format!("{item:?}")));
default_value
};
}
@ -186,10 +161,7 @@ impl LoadSaveStruct {
// Bool, int, string
pub fn save_var<T: ToString>(&mut self, key: String, value: &T) {
if self.loaded_items.contains_key(&key) {
add_text_to_text_view(
&self.text_view,
&flg!("saving_loading_saving_same_keys", generate_translation_hashmap(vec![("key", key.clone())])),
);
add_text_to_text_view(&self.text_view, &flg!("saving_loading_saving_same_keys", key = key.clone()));
}
self.loaded_items.insert(key, vec![value.to_string()]);
@ -224,10 +196,7 @@ impl LoadSaveStruct {
if !config_dir.is_dir() {
add_text_to_text_view(
text_view_errors,
&flg!(
"saving_loading_folder_config_instead_file",
generate_translation_hashmap(vec![("path", config_dir.to_string_lossy().to_string())])
),
&flg!("saving_loading_folder_config_instead_file", path = config_dir.to_string_lossy().to_string()),
);
return None;
}
@ -236,7 +205,8 @@ impl LoadSaveStruct {
text_view_errors,
&flg!(
"saving_loading_failed_to_create_configuration_folder",
generate_translation_hashmap(vec![("path", config_dir.to_string_lossy().to_string()), ("reason", e.to_string())])
path = config_dir.to_string_lossy().to_string(),
reason = e.to_string()
),
);
return None;
@ -249,7 +219,8 @@ impl LoadSaveStruct {
text_view_errors,
&flg!(
"saving_loading_failed_to_create_config_file",
generate_translation_hashmap(vec![("path", config_file.to_string_lossy().to_string()), ("reason", e.to_string())])
path = config_file.to_string_lossy().to_string(),
reason = e.to_string()
),
);
return None;
@ -262,10 +233,7 @@ impl LoadSaveStruct {
// Don't show errors when there is no configuration file when starting app
add_text_to_text_view(
text_view_errors,
&flg!(
"saving_loading_failed_to_read_config_file",
generate_translation_hashmap(vec![("path", config_file.to_string_lossy().to_string())])
),
&flg!("saving_loading_failed_to_read_config_file", path = config_file.to_string_lossy().to_string()),
);
}
return None;
@ -278,7 +246,8 @@ impl LoadSaveStruct {
text_view_errors,
&flg!(
"saving_loading_failed_to_create_config_file",
generate_translation_hashmap(vec![("path", config_file.to_string_lossy().to_string()), ("reason", e.to_string())])
path = config_file.to_string_lossy().to_string(),
reason = e.to_string()
),
);
return None;
@ -299,7 +268,8 @@ impl LoadSaveStruct {
text_view_errors,
&flg!(
"saving_loading_failed_to_read_data_from_file",
generate_translation_hashmap(vec![("path", config_file.to_string_lossy().to_string()), ("reason", e.to_string())])
path = config_file.to_string_lossy().to_string(),
reason = e.to_string()
),
);
return;
@ -314,23 +284,14 @@ impl LoadSaveStruct {
} else if !header.is_empty() {
self.loaded_items.entry(header.clone()).or_default().push(line.to_string());
} else {
add_text_to_text_view(
text_view_errors,
&flg!(
"saving_loading_orphan_data",
generate_translation_hashmap(vec![("data", line.to_string()), ("index", index.to_string())])
),
);
add_text_to_text_view(text_view_errors, &flg!("saving_loading_orphan_data", data = line, line = index.to_string()));
}
}
let (_, hashmap_sl) = create_hash_map();
for setting in self.loaded_items.keys() {
if !hashmap_sl.contains_key(setting) {
add_text_to_text_view(
text_view_errors,
&flg!("saving_loading_not_valid", generate_translation_hashmap(vec![("data", setting.to_string())])),
);
add_text_to_text_view(text_view_errors, &flg!("saving_loading_not_valid", data = setting.clone()));
}
}
@ -368,20 +329,12 @@ impl LoadSaveStruct {
if data_saved {
add_text_to_text_view(
text_view_errors,
flg!(
"saving_loading_saving_success",
generate_translation_hashmap(vec![("name", config_file.to_string_lossy().to_string())])
)
.as_str(),
flg!("saving_loading_saving_success", name = config_file.to_string_lossy().to_string()).as_str(),
);
} else {
add_text_to_text_view(
text_view_errors,
flg!(
"saving_loading_saving_failure",
generate_translation_hashmap(vec![("name", config_file.to_string_lossy().to_string())])
)
.as_str(),
flg!("saving_loading_saving_failure", name = config_file.to_string_lossy().to_string()).as_str(),
);
}
}
@ -509,183 +462,138 @@ pub fn save_configuration(manual_execution: bool, upper_notebook: &GuiUpperNoteb
// Upper notebook
saving_struct.save_list_store(
hashmap_ls.get(&LoadText::IncludedDirectories).unwrap().to_string(),
hashmap_ls[&LoadText::IncludedDirectories].clone(),
&upper_notebook.tree_view_included_directories.clone(),
ColumnsIncludedDirectory::Path as i32,
);
saving_struct.save_list_store(
hashmap_ls.get(&LoadText::ExcludedDirectories).unwrap().to_string(),
hashmap_ls[&LoadText::ExcludedDirectories].clone(),
&upper_notebook.tree_view_excluded_directories.clone(),
ColumnsExcludedDirectory::Path as i32,
);
saving_struct.save_var(hashmap_ls.get(&LoadText::ExcludedItems).unwrap().to_string(), &upper_notebook.entry_excluded_items.text());
saving_struct.save_var(
hashmap_ls.get(&LoadText::AllowedExtensions).unwrap().to_string(),
&upper_notebook.entry_allowed_extensions.text(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::MinimalFileSize).unwrap().to_string(),
&upper_notebook.entry_general_minimal_size.text(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::MaximalFileSize).unwrap().to_string(),
&upper_notebook.entry_general_maximal_size.text(),
);
saving_struct.save_var(hashmap_ls[&LoadText::ExcludedItems].clone(), &upper_notebook.entry_excluded_items.text());
saving_struct.save_var(hashmap_ls[&LoadText::AllowedExtensions].clone(), &upper_notebook.entry_allowed_extensions.text());
saving_struct.save_var(hashmap_ls[&LoadText::MinimalFileSize].clone(), &upper_notebook.entry_general_minimal_size.text());
saving_struct.save_var(hashmap_ls[&LoadText::MaximalFileSize].clone(), &upper_notebook.entry_general_maximal_size.text());
// Check buttons
saving_struct.save_var(hashmap_ls[&LoadText::SaveAtExit].clone(), &settings.check_button_settings_save_at_exit.is_active());
saving_struct.save_var(hashmap_ls[&LoadText::LoadAtStart].clone(), &settings.check_button_settings_load_at_start.is_active());
saving_struct.save_var(
hashmap_ls.get(&LoadText::SaveAtExit).unwrap().to_string(),
&settings.check_button_settings_save_at_exit.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::LoadAtStart).unwrap().to_string(),
&settings.check_button_settings_load_at_start.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ConfirmDeletionFiles).unwrap().to_string(),
hashmap_ls[&LoadText::ConfirmDeletionFiles].clone(),
&settings.check_button_settings_confirm_deletion.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ConfirmDeletionAllFilesInGroup).unwrap().to_string(),
hashmap_ls[&LoadText::ConfirmDeletionAllFilesInGroup].clone(),
&settings.check_button_settings_confirm_group_deletion.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ImagePreviewImage).unwrap().to_string(),
hashmap_ls[&LoadText::ImagePreviewImage].clone(),
&settings.check_button_settings_show_preview_similar_images.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::DuplicatePreviewImage).unwrap().to_string(),
hashmap_ls[&LoadText::DuplicatePreviewImage].clone(),
&settings.check_button_settings_show_preview_duplicates.is_active(),
);
saving_struct.save_var(hashmap_ls[&LoadText::HideHardLinks].clone(), &settings.check_button_settings_hide_hard_links.is_active());
saving_struct.save_var(hashmap_ls[&LoadText::UseCache].clone(), &settings.check_button_settings_use_cache.is_active());
saving_struct.save_var(hashmap_ls[&LoadText::UseJsonCacheFile].clone(), &settings.check_button_settings_save_also_json.is_active());
saving_struct.save_var(hashmap_ls[&LoadText::DeleteToTrash].clone(), &settings.check_button_settings_use_trash.is_active());
saving_struct.save_var(
hashmap_ls.get(&LoadText::HideHardLinks).unwrap().to_string(),
&settings.check_button_settings_hide_hard_links.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::UseCache).unwrap().to_string(),
&settings.check_button_settings_use_cache.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::UseJsonCacheFile).unwrap().to_string(),
&settings.check_button_settings_save_also_json.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::DeleteToTrash).unwrap().to_string(),
&settings.check_button_settings_use_trash.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ImageDeleteOutdatedCacheEntries).unwrap().to_string(),
hashmap_ls[&LoadText::ImageDeleteOutdatedCacheEntries].clone(),
&settings.check_button_settings_similar_images_delete_outdated_cache.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::DuplicateDeleteOutdatedCacheEntries).unwrap().to_string(),
hashmap_ls[&LoadText::DuplicateDeleteOutdatedCacheEntries].clone(),
&settings.check_button_settings_duplicates_delete_outdated_cache.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::VideoDeleteOutdatedCacheEntries).unwrap().to_string(),
hashmap_ls[&LoadText::VideoDeleteOutdatedCacheEntries].clone(),
&settings.check_button_settings_similar_videos_delete_outdated_cache.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::UsePrehashCache).unwrap().to_string(),
hashmap_ls[&LoadText::UsePrehashCache].clone(),
&settings.check_button_duplicates_use_prehash_cache.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ShowBottomTextPanel).unwrap().to_string(),
hashmap_ls[&LoadText::ShowBottomTextPanel].clone(),
&settings.check_button_settings_show_text_view.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::GeneralIgnoreOtherFilesystems).unwrap().to_string(),
hashmap_ls[&LoadText::GeneralIgnoreOtherFilesystems].clone(),
&settings.check_button_settings_one_filesystem.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::BrokenFilesArchive).unwrap().to_string(),
hashmap_ls[&LoadText::BrokenFilesArchive].clone(),
&main_notebook.check_button_broken_files_archive.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::BrokenFilesImage).unwrap().to_string(),
&main_notebook.check_button_broken_files_image.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::BrokenFilesAudio).unwrap().to_string(),
&main_notebook.check_button_broken_files_audio.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::BrokenFilesPdf).unwrap().to_string(),
&main_notebook.check_button_broken_files_pdf.is_active(),
);
saving_struct.save_var(hashmap_ls[&LoadText::BrokenFilesImage].clone(), &main_notebook.check_button_broken_files_image.is_active());
saving_struct.save_var(hashmap_ls[&LoadText::BrokenFilesAudio].clone(), &main_notebook.check_button_broken_files_audio.is_active());
saving_struct.save_var(hashmap_ls[&LoadText::BrokenFilesPdf].clone(), &main_notebook.check_button_broken_files_pdf.is_active());
// Others
saving_struct.save_var(hashmap_ls[&LoadText::ThreadNumber].clone(), &settings.scale_settings_number_of_threads.value().round());
saving_struct.save_var(hashmap_ls[&LoadText::MinimalCacheSize].clone(), &settings.entry_settings_cache_file_minimal_size.text());
saving_struct.save_var(
hashmap_ls.get(&LoadText::ThreadNumber).unwrap().to_string(),
&settings.scale_settings_number_of_threads.value().round(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::MinimalCacheSize).unwrap().to_string(),
&settings.entry_settings_cache_file_minimal_size.text(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::MinimalPrehashCacheSize).unwrap().to_string(),
hashmap_ls[&LoadText::MinimalPrehashCacheSize].clone(),
&settings.entry_settings_prehash_cache_file_minimal_size.text(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::Language).unwrap().to_string(),
hashmap_ls[&LoadText::Language].clone(),
&get_language_from_combo_box_text(&settings.combo_box_settings_language.active_text().unwrap()).short_text,
);
// Comboboxes main notebook
saving_struct.save_var(
hashmap_ls.get(&LoadText::ComboBoxDuplicateHashType).unwrap().to_string(),
hashmap_ls[&LoadText::ComboBoxDuplicateHashType].clone(),
&main_notebook.combo_box_duplicate_hash_type.active().unwrap_or(0),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ComboBoxDuplicateCheckMethod).unwrap().to_string(),
hashmap_ls[&LoadText::ComboBoxDuplicateCheckMethod].clone(),
&main_notebook.combo_box_duplicate_check_method.active().unwrap_or(0),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ComboBoxImageResizeAlgorithm).unwrap().to_string(),
hashmap_ls[&LoadText::ComboBoxImageResizeAlgorithm].clone(),
&main_notebook.combo_box_image_resize_algorithm.active().unwrap_or(0),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ComboBoxImageHashType).unwrap().to_string(),
hashmap_ls[&LoadText::ComboBoxImageHashType].clone(),
&main_notebook.combo_box_image_hash_algorithm.active().unwrap_or(0),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ComboBoxImageHashSize).unwrap().to_string(),
hashmap_ls[&LoadText::ComboBoxImageHashSize].clone(),
&main_notebook.combo_box_image_hash_size.active().unwrap_or(0),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::ComboBoxBigFiles).unwrap().to_string(),
hashmap_ls[&LoadText::ComboBoxBigFiles].clone(),
&main_notebook.combo_box_big_files_mode.active().unwrap_or(0),
);
// Other2
saving_struct.save_var(
hashmap_ls.get(&LoadText::DuplicateNameCaseSensitive).unwrap().to_string(),
hashmap_ls[&LoadText::DuplicateNameCaseSensitive].clone(),
&main_notebook.check_button_duplicate_case_sensitive_name.is_active(),
);
saving_struct.save_var(hashmap_ls[&LoadText::NumberOfBiggestFiles].clone(), &main_notebook.entry_big_files_number.text());
saving_struct.save_var(
hashmap_ls.get(&LoadText::NumberOfBiggestFiles).unwrap().to_string(),
&main_notebook.entry_big_files_number.text(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::SimilarImagesSimilarity).unwrap().to_string(),
hashmap_ls[&LoadText::SimilarImagesSimilarity].clone(),
&main_notebook.scale_similarity_similar_images.value(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::SimilarImagesIgnoreSameSize).unwrap().to_string(),
hashmap_ls[&LoadText::SimilarImagesIgnoreSameSize].clone(),
&main_notebook.check_button_image_ignore_same_size.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::SimilarVideosSimilarity).unwrap().to_string(),
hashmap_ls[&LoadText::SimilarVideosSimilarity].clone(),
&main_notebook.scale_similarity_similar_videos.value(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::SimilarVideosIgnoreSameSize).unwrap().to_string(),
hashmap_ls[&LoadText::SimilarVideosIgnoreSameSize].clone(),
&main_notebook.check_button_video_ignore_same_size.is_active(),
);
saving_struct.save_var(
hashmap_ls.get(&LoadText::MusicApproximateComparison).unwrap().to_string(),
hashmap_ls[&LoadText::MusicApproximateComparison].clone(),
&main_notebook.check_button_music_approximate_comparison.is_active(),
);
@ -719,83 +627,59 @@ pub fn load_configuration(
// Loading data from hashmaps
let (hashmap_ls, _hashmap_sl) = create_hash_map();
let mut included_directories: Vec<String> = loaded_entries.get_vector_string(hashmap_ls.get(&LoadText::IncludedDirectories).unwrap(), included_directories);
let mut excluded_directories: Vec<String> = loaded_entries.get_vector_string(hashmap_ls.get(&LoadText::ExcludedDirectories).unwrap(), excluded_directories);
let excluded_items: String = loaded_entries.get_string(
hashmap_ls.get(&LoadText::ExcludedItems).unwrap().clone(),
upper_notebook.entry_excluded_items.text().to_string(),
);
let allowed_extensions: String = loaded_entries.get_string(hashmap_ls.get(&LoadText::AllowedExtensions).unwrap().clone(), String::new());
let minimal_file_size: String = loaded_entries.get_integer_string(hashmap_ls.get(&LoadText::MinimalFileSize).unwrap().clone(), DEFAULT_MINIMAL_FILE_SIZE.to_string());
let maximal_file_size: String = loaded_entries.get_integer_string(hashmap_ls.get(&LoadText::MaximalFileSize).unwrap().clone(), DEFAULT_MAXIMAL_FILE_SIZE.to_string());
let mut included_directories: Vec<String> = loaded_entries.get_vector_string(&hashmap_ls[&LoadText::IncludedDirectories], included_directories);
let mut excluded_directories: Vec<String> = loaded_entries.get_vector_string(&hashmap_ls[&LoadText::ExcludedDirectories], excluded_directories);
let excluded_items: String = loaded_entries.get_string(hashmap_ls[&LoadText::ExcludedItems].clone(), upper_notebook.entry_excluded_items.text().to_string());
let allowed_extensions: String = loaded_entries.get_string(hashmap_ls[&LoadText::AllowedExtensions].clone(), String::new());
let minimal_file_size: String = loaded_entries.get_integer_string(hashmap_ls[&LoadText::MinimalFileSize].clone(), DEFAULT_MINIMAL_FILE_SIZE.to_string());
let maximal_file_size: String = loaded_entries.get_integer_string(hashmap_ls[&LoadText::MaximalFileSize].clone(), DEFAULT_MAXIMAL_FILE_SIZE.to_string());
let loading_at_start: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::LoadAtStart).unwrap().clone(), DEFAULT_LOAD_AT_START);
let mut saving_at_exit: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::SaveAtExit).unwrap().clone(), DEFAULT_SAVE_ON_EXIT);
let confirm_deletion: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::ConfirmDeletionFiles).unwrap().clone(), DEFAULT_CONFIRM_DELETION);
let confirm_group_deletion: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::ConfirmDeletionAllFilesInGroup).unwrap().clone(), DEFAULT_CONFIRM_GROUP_DELETION);
let show_previews_similar_images: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::ImagePreviewImage).unwrap().clone(), DEFAULT_SHOW_IMAGE_PREVIEW);
let show_previews_duplicates: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::DuplicatePreviewImage).unwrap().clone(), DEFAULT_SHOW_DUPLICATE_IMAGE_PREVIEW);
let bottom_text_panel: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::ShowBottomTextPanel).unwrap().clone(), DEFAULT_BOTTOM_TEXT_VIEW);
let hide_hard_links: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::HideHardLinks).unwrap().clone(), DEFAULT_HIDE_HARD_LINKS);
let use_cache: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::UseCache).unwrap().clone(), DEFAULT_USE_CACHE);
let use_json_cache: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::UseJsonCacheFile).unwrap().clone(), DEFAULT_SAVE_ALSO_AS_JSON);
let use_trash: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::DeleteToTrash).unwrap().clone(), DEFAULT_USE_TRASH);
let ignore_other_fs: bool = loaded_entries.get_bool(
hashmap_ls.get(&LoadText::GeneralIgnoreOtherFilesystems).unwrap().clone(),
DEFAULT_GENERAL_IGNORE_OTHER_FILESYSTEMS,
);
let loading_at_start: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::LoadAtStart].clone(), DEFAULT_LOAD_AT_START);
let mut saving_at_exit: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::SaveAtExit].clone(), DEFAULT_SAVE_ON_EXIT);
let confirm_deletion: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::ConfirmDeletionFiles].clone(), DEFAULT_CONFIRM_DELETION);
let confirm_group_deletion: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::ConfirmDeletionAllFilesInGroup].clone(), DEFAULT_CONFIRM_GROUP_DELETION);
let show_previews_similar_images: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::ImagePreviewImage].clone(), DEFAULT_SHOW_IMAGE_PREVIEW);
let show_previews_duplicates: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::DuplicatePreviewImage].clone(), DEFAULT_SHOW_DUPLICATE_IMAGE_PREVIEW);
let bottom_text_panel: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::ShowBottomTextPanel].clone(), DEFAULT_BOTTOM_TEXT_VIEW);
let hide_hard_links: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::HideHardLinks].clone(), DEFAULT_HIDE_HARD_LINKS);
let use_cache: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::UseCache].clone(), DEFAULT_USE_CACHE);
let use_json_cache: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::UseJsonCacheFile].clone(), DEFAULT_SAVE_ALSO_AS_JSON);
let use_trash: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::DeleteToTrash].clone(), DEFAULT_USE_TRASH);
let ignore_other_fs: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::GeneralIgnoreOtherFilesystems].clone(), DEFAULT_GENERAL_IGNORE_OTHER_FILESYSTEMS);
let delete_outdated_cache_duplicates: bool = loaded_entries.get_bool(
hashmap_ls.get(&LoadText::DuplicateDeleteOutdatedCacheEntries).unwrap().clone(),
hashmap_ls[&LoadText::DuplicateDeleteOutdatedCacheEntries].clone(),
DEFAULT_DUPLICATE_REMOVE_AUTO_OUTDATED_CACHE,
);
let delete_outdated_cache_similar_images: bool = loaded_entries.get_bool(
hashmap_ls.get(&LoadText::ImageDeleteOutdatedCacheEntries).unwrap().clone(),
DEFAULT_IMAGE_REMOVE_AUTO_OUTDATED_CACHE,
);
let delete_outdated_cache_similar_videos: bool = loaded_entries.get_bool(
hashmap_ls.get(&LoadText::VideoDeleteOutdatedCacheEntries).unwrap().clone(),
DEFAULT_VIDEO_REMOVE_AUTO_OUTDATED_CACHE,
);
let use_prehash_cache: bool = loaded_entries.get_bool(hashmap_ls.get(&LoadText::UsePrehashCache).unwrap().clone(), DEFAULT_USE_PRECACHE);
let delete_outdated_cache_similar_images: bool =
loaded_entries.get_bool(hashmap_ls[&LoadText::ImageDeleteOutdatedCacheEntries].clone(), DEFAULT_IMAGE_REMOVE_AUTO_OUTDATED_CACHE);
let delete_outdated_cache_similar_videos: bool =
loaded_entries.get_bool(hashmap_ls[&LoadText::VideoDeleteOutdatedCacheEntries].clone(), DEFAULT_VIDEO_REMOVE_AUTO_OUTDATED_CACHE);
let use_prehash_cache: bool = loaded_entries.get_bool(hashmap_ls[&LoadText::UsePrehashCache].clone(), DEFAULT_USE_PRECACHE);
let cache_prehash_minimal_size: String = loaded_entries.get_integer_string(
hashmap_ls.get(&LoadText::MinimalPrehashCacheSize).unwrap().clone(),
DEFAULT_PREHASH_MINIMAL_CACHE_SIZE.to_string(),
);
let cache_minimal_size: String = loaded_entries.get_integer_string(hashmap_ls.get(&LoadText::MinimalCacheSize).unwrap().clone(), DEFAULT_MINIMAL_CACHE_SIZE.to_string());
let short_language = loaded_entries.get_string(hashmap_ls.get(&LoadText::Language).unwrap().clone(), short_language);
let cache_prehash_minimal_size: String =
loaded_entries.get_integer_string(hashmap_ls[&LoadText::MinimalPrehashCacheSize].clone(), DEFAULT_PREHASH_MINIMAL_CACHE_SIZE.to_string());
let cache_minimal_size: String = loaded_entries.get_integer_string(hashmap_ls[&LoadText::MinimalCacheSize].clone(), DEFAULT_MINIMAL_CACHE_SIZE.to_string());
let short_language = loaded_entries.get_string(hashmap_ls[&LoadText::Language].clone(), short_language);
let combo_box_duplicate_hash_type = loaded_entries.get_object(hashmap_ls.get(&LoadText::ComboBoxDuplicateHashType).unwrap().clone(), 0);
let combo_box_duplicate_checking_method = loaded_entries.get_object(hashmap_ls.get(&LoadText::ComboBoxDuplicateCheckMethod).unwrap().clone(), 0);
let combo_box_image_hash_size = loaded_entries.get_object(hashmap_ls.get(&LoadText::ComboBoxImageHashSize).unwrap().clone(), 1); // 16 instead default 8
let combo_box_image_hash_algorithm = loaded_entries.get_object(hashmap_ls.get(&LoadText::ComboBoxImageHashType).unwrap().clone(), 0);
let combo_box_image_resize_algorithm = loaded_entries.get_object(hashmap_ls.get(&LoadText::ComboBoxImageResizeAlgorithm).unwrap().clone(), 0);
let combo_box_big_files_mode = loaded_entries.get_object(hashmap_ls.get(&LoadText::ComboBoxBigFiles).unwrap().clone(), 0);
let combo_box_duplicate_hash_type = loaded_entries.get_object(hashmap_ls[&LoadText::ComboBoxDuplicateHashType].clone(), 0);
let combo_box_duplicate_checking_method = loaded_entries.get_object(hashmap_ls[&LoadText::ComboBoxDuplicateCheckMethod].clone(), 0);
let combo_box_image_hash_size = loaded_entries.get_object(hashmap_ls[&LoadText::ComboBoxImageHashSize].clone(), 1); // 16 instead default 8
let combo_box_image_hash_algorithm = loaded_entries.get_object(hashmap_ls[&LoadText::ComboBoxImageHashType].clone(), 0);
let combo_box_image_resize_algorithm = loaded_entries.get_object(hashmap_ls[&LoadText::ComboBoxImageResizeAlgorithm].clone(), 0);
let combo_box_big_files_mode = loaded_entries.get_object(hashmap_ls[&LoadText::ComboBoxBigFiles].clone(), 0);
let number_of_biggest_files = loaded_entries.get_integer_string(
hashmap_ls.get(&LoadText::NumberOfBiggestFiles).unwrap().clone(),
DEFAULT_NUMBER_OF_BIGGEST_FILES.to_string(),
);
let similar_images_similarity = loaded_entries.get_object(hashmap_ls.get(&LoadText::SimilarImagesSimilarity).unwrap().clone(), DEFAULT_SIMILAR_IMAGES_SIMILARITY);
let similar_images_ignore_same_size = loaded_entries.get_bool(
hashmap_ls.get(&LoadText::SimilarImagesIgnoreSameSize).unwrap().clone(),
DEFAULT_SIMILAR_IMAGES_IGNORE_SAME_SIZE,
);
let similar_videos_similarity = loaded_entries.get_object(hashmap_ls.get(&LoadText::SimilarVideosSimilarity).unwrap().clone(), DEFAULT_SIMILAR_VIDEOS_SIMILARITY);
let similar_videos_ignore_same_size = loaded_entries.get_bool(
hashmap_ls.get(&LoadText::SimilarVideosIgnoreSameSize).unwrap().clone(),
DEFAULT_SIMILAR_VIDEOS_IGNORE_SAME_SIZE,
);
let check_button_case_sensitive_name = loaded_entries.get_object(
hashmap_ls.get(&LoadText::DuplicateNameCaseSensitive).unwrap().clone(),
DEFAULT_DUPLICATE_CASE_SENSITIVE_NAME_CHECKING,
);
let number_of_biggest_files = loaded_entries.get_integer_string(hashmap_ls[&LoadText::NumberOfBiggestFiles].clone(), DEFAULT_NUMBER_OF_BIGGEST_FILES.to_string());
let similar_images_similarity = loaded_entries.get_object(hashmap_ls[&LoadText::SimilarImagesSimilarity].clone(), DEFAULT_SIMILAR_IMAGES_SIMILARITY);
let similar_images_ignore_same_size = loaded_entries.get_bool(hashmap_ls[&LoadText::SimilarImagesIgnoreSameSize].clone(), DEFAULT_SIMILAR_IMAGES_IGNORE_SAME_SIZE);
let similar_videos_similarity = loaded_entries.get_object(hashmap_ls[&LoadText::SimilarVideosSimilarity].clone(), DEFAULT_SIMILAR_VIDEOS_SIMILARITY);
let similar_videos_ignore_same_size = loaded_entries.get_bool(hashmap_ls[&LoadText::SimilarVideosIgnoreSameSize].clone(), DEFAULT_SIMILAR_VIDEOS_IGNORE_SAME_SIZE);
let check_button_case_sensitive_name = loaded_entries.get_object(hashmap_ls[&LoadText::DuplicateNameCaseSensitive].clone(), DEFAULT_DUPLICATE_CASE_SENSITIVE_NAME_CHECKING);
let check_button_broken_files_archive = loaded_entries.get_object(hashmap_ls.get(&LoadText::BrokenFilesArchive).unwrap().clone(), DEFAULT_BROKEN_FILES_ARCHIVE);
let check_button_broken_files_pdf = loaded_entries.get_object(hashmap_ls.get(&LoadText::BrokenFilesPdf).unwrap().clone(), DEFAULT_BROKEN_FILES_PDF);
let check_button_broken_files_image = loaded_entries.get_object(hashmap_ls.get(&LoadText::BrokenFilesImage).unwrap().clone(), DEFAULT_BROKEN_FILES_IMAGE);
let check_button_broken_files_audio = loaded_entries.get_object(hashmap_ls.get(&LoadText::BrokenFilesAudio).unwrap().clone(), DEFAULT_BROKEN_FILES_AUDIO);
let thread_number = loaded_entries.get_object(hashmap_ls.get(&LoadText::ThreadNumber).unwrap().clone(), DEFAULT_THREAD_NUMBER);
let check_button_broken_files_archive = loaded_entries.get_object(hashmap_ls[&LoadText::BrokenFilesArchive].clone(), DEFAULT_BROKEN_FILES_ARCHIVE);
let check_button_broken_files_pdf = loaded_entries.get_object(hashmap_ls[&LoadText::BrokenFilesPdf].clone(), DEFAULT_BROKEN_FILES_PDF);
let check_button_broken_files_image = loaded_entries.get_object(hashmap_ls[&LoadText::BrokenFilesImage].clone(), DEFAULT_BROKEN_FILES_IMAGE);
let check_button_broken_files_audio = loaded_entries.get_object(hashmap_ls[&LoadText::BrokenFilesAudio].clone(), DEFAULT_BROKEN_FILES_AUDIO);
let thread_number = loaded_entries.get_object(hashmap_ls[&LoadText::ThreadNumber].clone(), DEFAULT_THREAD_NUMBER);
let mut set_start_folders = false;
if !manual_execution {
@ -949,8 +833,8 @@ pub fn load_configuration(
main_notebook.scale_similarity_similar_images.connect_change_value(scale_step_function);
main_notebook.scale_similarity_similar_images.set_value(similar_images_similarity as f64);
settings.scale_settings_number_of_threads.set_range(0_f64, get_default_number_of_threads() as f64);
settings.scale_settings_number_of_threads.set_fill_level(get_default_number_of_threads() as f64);
settings.scale_settings_number_of_threads.set_range(0_f64, get_all_available_threads() as f64);
settings.scale_settings_number_of_threads.set_fill_level(get_all_available_threads() as f64);
settings.scale_settings_number_of_threads.connect_change_value(scale_step_function);
settings.scale_settings_number_of_threads.set_value(thread_number as f64);
} else {

View file

@ -4,7 +4,7 @@
<!-- interface-name about_dialog.ui -->
<requires lib="gtk" version="4.6"/>
<object class="GtkAboutDialog" id="about_dialog">
<property name="comments" translatable="yes">2020 - 2023 Rafał Mikrut(qarmin)
<property name="comments" translatable="yes">2020 - 2024 Rafał Mikrut(qarmin)
This program is free to use and will always be.
</property>

View file

@ -242,6 +242,8 @@
(5,237,"GtkScale","scale_similarity_same_music",117,None,None,None,9,None),
(5,238,"GtkLabel","label_same_music_seconds",117,None,None,None,6,None),
(5,239,"GtkLabel","label_same_music_similarity",117,None,None,None,8,None),
(5,240,"GtkLabel","label_excluded_extensions",46,None,None,None,2,None),
(5,241,"GtkEntry","entry_excluded_extensions",46,None,None,None,3,None),
(6,1,"GtkPopover","popover_right_click",None,None,None,None,None,None),
(6,2,"GtkBox",None,1,None,None,None,None,None),
(6,3,"GtkButton","buttons_popover_right_click_open_file",2,None,None,None,None,None),
@ -265,7 +267,7 @@
(7,17,"GtkButton","buttons_popover_unselect_all",2,None,None,None,14,None),
(8,15,"GtkDialog","window_progress",None,None,None,None,None,None),
(8,16,"GtkBox",None,15,None,None,None,None,None),
(8,17,"GtkGrid","grid_progress_stages",16,None,None,None,None,None),
(8,17,"GtkGrid","grid_progress",16,None,None,None,None,None),
(8,18,"GtkLabel","label_progress_all_stages",17,None,None,None,None,None),
(8,19,"GtkProgressBar","progress_bar_all_stages",17,None,None,None,1,None),
(8,20,"GtkLabel","label_progress_current_stage",17,None,None,None,2,None),
@ -340,7 +342,7 @@
(10,7,"GtkButton","buttons_popover_sort_selection",2,None,None,None,4,None)
</object>
<object_property>
(3,1,"GtkAboutDialog","comments","2020 - 2023 Rafał Mikrut(qarmin)
(3,1,"GtkAboutDialog","comments","2020 - 2024 Rafał Mikrut(qarmin)
This program is free to use and will always be.
",1,None,None,None,None,None,None,None,None),
@ -797,6 +799,9 @@
(5,239,"GtkLabel","label","Max difference",None,None,None,None,None,None,None,None,None),
(5,239,"GtkWidget","margin-end","5",None,None,None,None,None,None,None,None,None),
(5,239,"GtkWidget","margin-start","5",None,None,None,None,None,None,None,None,None),
(5,240,"GtkLabel","label","Disabled Extensions","yes",None,None,None,None,None,None,None,None),
(5,241,"GtkWidget","focusable","1",None,None,None,None,None,None,None,None,None),
(5,241,"GtkWidget","hexpand","1",None,None,None,None,None,None,None,None,None),
(6,1,"GtkPopover","child",None,None,None,None,None,2,None,None,None,None),
(6,1,"GtkPopover","position","left",None,None,None,None,None,None,None,None,None),
(6,2,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None),

View file

@ -269,6 +269,17 @@
<property name="hexpand">1</property>
</object>
</child>
<child>
<object class="GtkLabel" id="label_excluded_extensions">
<property name="label" translatable="yes">Disabled Extensions</property>
</object>
</child>
<child>
<object class="GtkEntry" id="entry_excluded_extensions">
<property name="focusable">1</property>
<property name="hexpand">1</property>
</object>
</child>
</object>
</child>
<child>

View file

@ -13,7 +13,7 @@
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkGrid" id="grid_progress_stages">
<object class="GtkGrid" id="grid_progress">
<property name="margin-end">2</property>
<property name="margin-start">2</property>
<property name="margin-top">2</property>

View file

@ -19,7 +19,7 @@ New versions of GTK fixes some bugs, so e.g. middle button selection will work o
| Program | Min | What for |
|---------|--------|--------------------------------------------------------------------------------------|
| Rust | 1.72.1 | The minimum version of rust does not depend on anything, so it can change frequently |
| Rust | 1.74.0 | The minimum version of rust does not depend on anything, so it can change frequently |
| GTK | 4.6 | Only for the `GTK` backend |
#### Debian / Ubuntu

View file

@ -3,7 +3,7 @@ name = "krokiet"
version = "6.1.0"
authors = ["Rafał Mikrut <mikrutrafal@protonmail.com>"]
edition = "2021"
rust-version = "1.72.1"
rust-version = "1.74.0"
description = "Slint frontend of Czkawka Core"
license = "GPL-3"
homepage = "https://github.com/qarmin/czkawka"
@ -13,11 +13,11 @@ build = "build.rs"
[dependencies]
rand = "0.8"
czkawka_core = { version = "6.1.0", path = "../czkawka_core" }
chrono = "0.4.31"
chrono = "0.4.34"
open = "5.0"
crossbeam-channel = "0.5"
handsome_logger = "0.8"
rfd = { version = "0.12", default-features = false, features = ["xdg-portal"] }
rfd = { version = "0.13", default-features = false, features = ["xdg-portal", "async-std"] }
home = "0.5"
log = "0.4.20"
serde = "1.0"
@ -26,26 +26,26 @@ humansize = "2.1"
image = "0.24"
directories-next = "2.0"
image_hasher = "1.2"
rayon = "1.8.0"
rayon = "1.8"
# Translations
i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.7"
i18n-embed-fl = "0.8"
rust-embed = { version = "8.2", features = ["debug-embed"] }
once_cell = "1.19"
# Try to use only needed features from https://github.com/slint-ui/slint/blob/master/api/rs/slint/Cargo.toml#L23-L31
#slint = { path = "/home/rafal/test/slint/api/rs/slint/", default-features = false, features = ["std",
slint = { git = "https://github.com/slint-ui/slint.git", default-features = false, features = [
# slint = { version = "1.3", default-features = false, features = [
#slint = { git = "https://github.com/slint-ui/slint.git", default-features = false, features = [
slint = { version = "1.4", default-features = false, features = [
"std",
"backend-winit",
"compat-1-2"
] }
[build-dependencies]
#slint-build = { path = "/home/rafal/test/slint/api/rs/build/"}
slint-build = { git = "https://github.com/slint-ui/slint.git" }
# slint-build = "1.3"
#slint-build = { git = "https://github.com/slint-ui/slint.git" }
slint-build = "1.4"
[features]
default = ["winit_femtovg", "winit_software"]

View file

@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
Czkawka Slint Gui Copyright (C) 2023 Rafał Mikrut
Czkawka Slint Gui Copyright (C) 2024 Rafał Mikrut
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2023 Rafał Mikrut
Copyright (c) 2020-2024 Rafał Mikrut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,6 +1,6 @@
# Krokiet
Krokiet is new Czkawka frontend written in Slint(written mostly in Rust) in opposite to Gtk 4 frontend which uses mostly
Krokiet is new Czkawka frontend written in Slint, which uses Rust in opposite to Gtk 4 frontend which uses mostly
C code.
Different toolkit means different look, limitations and features, so you should not expect same features like in Gtk 4
@ -24,7 +24,7 @@ sudo apt install libfontconfig-dev libfreetype-dev
Default compilation is done by `cargo build --release` and should work on most systems.
You need the latest available version of Rust to compile it, because Krokiet aims to support the latest slint verions,
You need the latest available version of Rust to compile it, because Krokiet aims to support the latest slint versions,
that should provide best experience.
The only exception is building skia renderer which is non default feature that can be enabled manually if you want to
@ -103,25 +103,6 @@ SLINT_STYLE=material-dark cargo run -- --path .
preview - [slint live preview example](https://slint.dev/releases/1.3.0/editor/?load_demo=examples/printerdemo/ui/printerdemo.slint)
- Improving app rust code
## Missing features available in GTK 4 frontend
- icons in buttons
- resizable input files panel
- settings
- moving files
- deleting files
- sorting files
- saving results
- symlink/hardlink
- implementing all modes
- multiple selection
- proper popup windows - slint not handle them properly
- logo
- about window
- reference folders
- translations(problem is only with interface, messages like "Checking {x} file" can be easily translated from rust
side)
## Why Slint?
There are multiple reasons why I decided to use Slint as toolkit for Krokiet over other toolkits.

View file

@ -1,9 +1,10 @@
use crate::CurrentTab;
use slint::{ModelRc, SharedString, StandardListViewItem, VecModel};
use std::path::PathBuf;
use crate::{CurrentTab, ExcludedDirectoriesModel, IncludedDirectoriesModel, MainListModel, MainWindow};
use slint::{ModelRc, SharedString, VecModel};
// Remember to match updated this according to ui/main_lists.slint and connect_scan.rs files
pub fn get_path_idx(active_tab: CurrentTab) -> usize {
pub fn get_str_path_idx(active_tab: CurrentTab) -> usize {
match active_tab {
CurrentTab::EmptyFolders => 1,
CurrentTab::EmptyFiles => 1,
@ -11,7 +12,7 @@ pub fn get_path_idx(active_tab: CurrentTab) -> usize {
CurrentTab::Settings => panic!("Button should be disabled"),
}
}
pub fn get_name_idx(active_tab: CurrentTab) -> usize {
pub fn get_str_name_idx(active_tab: CurrentTab) -> usize {
match active_tab {
CurrentTab::EmptyFolders => 0,
CurrentTab::EmptyFiles => 0,
@ -19,6 +20,39 @@ pub fn get_name_idx(active_tab: CurrentTab) -> usize {
CurrentTab::Settings => 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"),
}
}
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"),
}
}
pub fn get_int_width_idx(active_tab: CurrentTab) -> usize {
match active_tab {
CurrentTab::SimilarImages => 4,
CurrentTab::Settings => 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"),
_ => 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,
@ -27,6 +61,24 @@ pub fn get_is_header_mode(active_tab: CurrentTab) -> bool {
}
}
pub fn get_tool_model(app: &MainWindow, tab: CurrentTab) -> ModelRc<MainListModel> {
match tab {
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"),
}
}
pub fn set_tool_model(app: &MainWindow, tab: CurrentTab, model: ModelRc<MainListModel>) {
match tab {
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"),
}
}
// pub fn create_string_standard_list_view(items: &[String]) -> ModelRc<StandardListViewItem> {
// let new_folders_standard_list_view = items
// .iter()
@ -38,18 +90,98 @@ pub fn get_is_header_mode(active_tab: CurrentTab) -> bool {
// .collect::<Vec<_>>();
// ModelRc::new(VecModel::from(new_folders_standard_list_view))
// }
pub fn create_string_standard_list_view_from_pathbuf(items: &[PathBuf]) -> ModelRc<StandardListViewItem> {
let new_folders_standard_list_view = items
// pub fn create_string_standard_list_view_from_pathbuf(items: &[PathBuf]) -> ModelRc<StandardListViewItem> {
// let new_folders_standard_list_view = items
// .iter()
// .map(|x| {
// let mut element = StandardListViewItem::default();
// element.text = x.to_string_lossy().to_string().into();
// element
// })
// .collect::<Vec<_>>();
// ModelRc::new(VecModel::from(new_folders_standard_list_view))
// }
pub fn create_included_directories_model_from_pathbuf(items: &[PathBuf], referenced: &[PathBuf]) -> ModelRc<IncludedDirectoriesModel> {
let referenced_as_string = referenced.iter().map(|x| x.to_string_lossy().to_string()).collect::<Vec<_>>();
let converted = items
.iter()
.map(|x| {
let mut element = StandardListViewItem::default();
element.text = x.to_string_lossy().to_string().into();
element
let path_as_string = x.to_string_lossy().to_string();
IncludedDirectoriesModel {
path: x.to_string_lossy().to_string().into(),
referenced_folder: referenced_as_string.contains(&path_as_string),
selected_row: false,
}
})
.collect::<Vec<_>>();
ModelRc::new(VecModel::from(new_folders_standard_list_view))
ModelRc::new(VecModel::from(converted))
}
pub fn create_excluded_directories_model_from_pathbuf(items: &[PathBuf]) -> ModelRc<ExcludedDirectoriesModel> {
let converted = items
.iter()
.map(|x| ExcludedDirectoriesModel {
path: x.to_string_lossy().to_string().into(),
selected_row: false,
})
.collect::<Vec<_>>();
ModelRc::new(VecModel::from(converted))
}
pub fn create_vec_model_from_vec_string(items: Vec<String>) -> VecModel<SharedString> {
VecModel::from(items.into_iter().map(SharedString::from).collect::<Vec<_>>())
}
// Workaround for https://github.com/slint-ui/slint/discussions/4596
// Currently there is no way to save u64 in slint, so we need to split it into two i32
pub fn split_u64_into_i32s(value: u64) -> (i32, i32) {
let part1: i32 = (value >> 32) as 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)
}
#[cfg(test)]
mod test {
use crate::common::split_u64_into_i32s;
#[test]
fn test_split_u64_into_i32s_small() {
let value = 1;
let (part1, part2) = split_u64_into_i32s(value);
assert_eq!(part1, 0);
assert_eq!(part2, 1);
}
#[test]
fn test_split_u64_into_i32s_big() {
let value = u64::MAX;
let (part1, part2) = split_u64_into_i32s(value);
assert_eq!(part1, -1);
assert_eq!(part2, -1);
}
#[test]
fn test_connect_i32_into_u64_small() {
let part1 = 0;
let part2 = 1;
let value = super::connect_i32_into_u64(part1, part2);
assert_eq!(value, 1);
}
#[test]
fn test_connect_i32_into_u64_big() {
let part1 = -1;
let part2 = -1;
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] {
let (part1, part2) = split_u64_into_i32s(start_value);
let end_value = super::connect_i32_into_u64(part1, part2);
assert_eq!(start_value, end_value);
}
}
}

View file

@ -1,9 +1,12 @@
use slint::{ComponentHandle, Model, ModelRc, VecModel};
use crate::common::{get_is_header_mode, get_name_idx, get_path_idx};
use crate::{Callabler, CurrentTab, GuiState, MainListModel, MainWindow};
use czkawka_core::common::{remove_folder_if_contains_only_empty_folders, CHARACTER};
use rayon::prelude::*;
use slint::{ComponentHandle, ModelRc, VecModel};
use czkawka_core::common::remove_folder_if_contains_only_empty_folders;
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};
pub fn connect_delete_button(app: &MainWindow) {
let a = app.as_weak();
@ -12,263 +15,56 @@ pub fn connect_delete_button(app: &MainWindow) {
let active_tab = app.global::<GuiState>().get_active_tab();
let model = match active_tab {
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"),
};
let model = get_tool_model(&app, active_tab);
let new_model = handle_delete_items(&model, active_tab);
let remove_to_trash = false;
let (errors, new_model) = handle_delete_items(&model, active_tab, remove_to_trash);
if let Some(new_model) = new_model {
match active_tab {
CurrentTab::EmptyFolders => app.set_empty_folder_model(new_model),
CurrentTab::SimilarImages => app.set_similar_images_model(new_model),
CurrentTab::EmptyFiles => app.set_empty_files_model(new_model),
CurrentTab::Settings => panic!("Button should be disabled"),
}
set_tool_model(&app, active_tab, new_model);
}
app.global::<GuiState>().set_info_text(Messages::new_from_errors(errors).create_messages_text().into());
app.global::<GuiState>().set_preview_visible(false);
});
}
fn handle_delete_items(items: &ModelRc<MainListModel>, active_tab: CurrentTab) -> Option<ModelRc<MainListModel>> {
fn handle_delete_items(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() {
remove_selected_items(entries_to_delete, active_tab);
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);
let r = ModelRc::new(VecModel::from(entries_left)); // TODO here maybe should also stay old model if entries cannot be removed
return Some(r);
return (errors, Some(r));
}
None
(vec![], None)
}
// 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: Vec<MainListModel>, active_tab: CurrentTab) {
let path_idx = get_path_idx(active_tab);
let name_idx = get_name_idx(active_tab);
let items_to_remove = items
.iter()
.map(|item| {
let path = item.val.iter().nth(path_idx).unwrap();
let name = item.val.iter().nth(name_idx).unwrap();
format!("{}{}{}", path, CHARACTER, name)
})
.collect::<Vec<_>>();
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 {
items_to_remove.into_par_iter().for_each(|item| {
remove_folder_if_contains_only_empty_folders(item);
});
items_to_remove
.into_par_iter()
.filter_map(|item| remove_folder_if_contains_only_empty_folders(item, remove_to_trash).err())
.collect()
} else {
items_to_remove.into_par_iter().for_each(|item| {
let _ = std::fs::remove_file(item);
});
}
}
fn deselect_all_items(items: &mut [MainListModel]) {
for item in items {
item.selected_row = false;
}
}
fn filter_out_checked_items(items: &ModelRc<MainListModel>, have_header: bool) -> (Vec<MainListModel>, Vec<MainListModel>) {
if cfg!(debug_assertions) {
check_if_header_is_checked(items);
check_if_header_is_selected_but_should_not_be(items, have_header);
}
let (entries_to_delete, mut entries_left): (Vec<_>, Vec<_>) = items.iter().partition(|item| item.checked);
// When have header, we must also throw out orphaned items - this needs to be
if have_header && !entries_left.is_empty() {
// First row must be header
assert!(entries_left[0].header_row);
if entries_left.len() == 3 {
// First row is header, so if second or third is also header, then there is no enough items to fill model
if entries_left[1].header_row || entries_left[2].header_row {
entries_left = Vec::new();
}
} else if entries_left.len() < 3 {
// Not have enough items to fill model
entries_left = Vec::new();
} else {
let mut last_header = 0;
let mut new_items: Vec<MainListModel> = Vec::new();
for i in 1..entries_left.len() {
if entries_left[i].header_row {
if i - last_header > 2 {
new_items.extend(entries_left[last_header..i].iter().cloned());
}
last_header = i;
items_to_remove
.into_par_iter()
.filter_map(|item| {
if let Err(e) = std::fs::remove_file(item) {
return Some(format!("Error while removing file: {e}"));
}
}
if entries_left.len() - last_header > 2 {
new_items.extend(entries_left[last_header..].iter().cloned());
}
entries_left = new_items;
}
}
(entries_to_delete, entries_left)
}
// Function to verify if really headers are not checked
// Checked header is big bug
fn check_if_header_is_checked(items: &ModelRc<MainListModel>) {
if cfg!(debug_assertions) {
for item in items.iter() {
if item.header_row {
assert!(!item.checked);
}
}
}
}
// In some modes header should not be visible, but if are, then it is a bug
fn check_if_header_is_selected_but_should_not_be(items: &ModelRc<MainListModel>, can_have_header: bool) {
if cfg!(debug_assertions) {
if !can_have_header {
for item in items.iter() {
assert!(!item.header_row);
}
}
}
}
#[cfg(test)]
mod tests {
use slint::{Model, ModelRc, SharedString, VecModel};
use crate::connect_delete::filter_out_checked_items;
use crate::MainListModel;
#[test]
fn test_filter_out_checked_items_empty() {
let items: ModelRc<MainListModel> = create_new_model(vec![]);
let (to_delete, left) = filter_out_checked_items(&items, false);
assert!(to_delete.is_empty());
assert!(left.is_empty());
let (to_delete, left) = filter_out_checked_items(&items, true);
assert!(to_delete.is_empty());
assert!(left.is_empty());
}
#[test]
fn test_filter_out_checked_items_one_element_valid_normal() {
let items = create_new_model(vec![(false, false, false, vec![])]);
let (to_delete, left) = filter_out_checked_items(&items, false);
assert!(to_delete.is_empty());
assert_eq!(left.len(), items.iter().count());
}
#[test]
fn test_filter_out_checked_items_one_element_valid_header() {
let items = create_new_model(vec![(false, true, false, vec![])]);
let (to_delete, left) = filter_out_checked_items(&items, true);
assert!(to_delete.is_empty());
assert!(left.is_empty());
}
#[test]
#[should_panic]
fn test_filter_out_checked_items_one_element_invalid_normal() {
let items = create_new_model(vec![(false, true, false, vec![])]);
filter_out_checked_items(&items, false);
}
#[test]
#[should_panic]
fn test_filter_out_checked_items_one_element_invalid_header() {
let items = create_new_model(vec![(false, false, false, vec![])]);
filter_out_checked_items(&items, true);
}
#[test]
fn test_filter_out_checked_items_multiple_element_valid_normal() {
let items = create_new_model(vec![
(false, false, false, vec!["1"]),
(false, false, false, vec!["2"]),
(true, false, false, vec!["3"]),
(true, false, false, vec!["4"]),
(false, false, false, vec!["5"]),
]);
let (to_delete, left) = filter_out_checked_items(&items, false);
let to_delete_data = get_single_data_from_model(&to_delete);
let left_data = get_single_data_from_model(&left);
assert_eq!(to_delete_data, vec!["3", "4"]);
assert_eq!(left_data, vec!["1", "2", "5"]);
}
#[test]
fn test_filter_out_checked_items_multiple_element_valid_header() {
let items = create_new_model(vec![
(false, true, false, vec!["1"]),
(false, false, false, vec!["2"]),
(true, false, false, vec!["3"]),
(false, true, false, vec!["4"]),
(false, false, false, vec!["5"]),
(false, true, false, vec!["6"]),
(false, false, false, vec!["7"]),
(false, false, false, vec!["8"]),
]);
let (to_delete, left) = filter_out_checked_items(&items, true);
let to_delete_data = get_single_data_from_model(&to_delete);
let left_data = get_single_data_from_model(&left);
assert_eq!(to_delete_data, vec!["3"]);
assert_eq!(left_data, vec!["6", "7", "8"]);
}
#[test]
fn test_filter_out_checked_items_multiple2_element_valid_header() {
let items = create_new_model(vec![
(false, true, false, vec!["1"]),
(false, false, false, vec!["2"]),
(true, false, false, vec!["3"]),
(false, false, false, vec!["4"]),
(false, false, false, vec!["5"]),
(false, false, false, vec!["6"]),
(false, true, false, vec!["7"]),
(false, false, false, vec!["8"]),
]);
let (to_delete, left) = filter_out_checked_items(&items, true);
let to_delete_data = get_single_data_from_model(&to_delete);
let left_data = get_single_data_from_model(&left);
assert_eq!(to_delete_data, vec!["3"]);
assert_eq!(left_data, vec!["1", "2", "4", "5", "6"]);
}
fn get_single_data_from_model(model: &[MainListModel]) -> Vec<String> {
let mut d = model.iter().map(|item| item.val.iter().next().unwrap().to_string()).collect::<Vec<_>>();
d.sort();
d
}
fn create_new_model(items: Vec<(bool, bool, bool, Vec<&'static str>)>) -> ModelRc<MainListModel> {
let model = VecModel::default();
for item in items {
let all_items: Vec<SharedString> = item.3.iter().map(|item| (*item).into()).collect::<Vec<_>>();
let all_items = VecModel::from(all_items);
model.push(MainListModel {
checked: item.0,
header_row: item.1,
selected_row: item.2,
val: ModelRc::new(all_items),
});
}
ModelRc::new(model)
None
})
.collect()
}
}

View file

@ -1,7 +1,7 @@
use rfd::FileDialog;
use slint::{ComponentHandle, Model, ModelRc, VecModel};
use crate::{Callabler, MainWindow, Settings};
use crate::{Callabler, ExcludedDirectoriesModel, IncludedDirectoriesModel, MainWindow, Settings};
pub fn connect_add_remove_directories(app: &MainWindow) {
connect_add_directories(app);
@ -12,66 +12,67 @@ pub fn connect_add_remove_directories(app: &MainWindow) {
fn connect_add_manual_directories(app: &MainWindow) {
let a = app.as_weak();
app.global::<Callabler>().on_added_manual_directories(move |included_directories, list_of_files_to_add| {
let non_empty_lines = list_of_files_to_add.lines().filter(|x| !x.is_empty()).collect::<Vec<_>>();
if non_empty_lines.is_empty() {
let folders = list_of_files_to_add.lines().filter(|x| !x.is_empty()).map(str::to_string).collect::<Vec<_>>();
if folders.is_empty() {
return;
}
let app = a.upgrade().unwrap();
let settings = app.global::<Settings>();
if included_directories {
let included_model = settings.get_included_directories();
let mut included_model = included_model.iter().collect::<Vec<_>>();
included_model.extend(non_empty_lines.iter().map(|x| {
let mut element = slint::StandardListViewItem::default();
element.text = (*x).into();
element
}));
included_model.sort_by_cached_key(|x| x.text.to_string());
included_model.dedup();
settings.set_included_directories(ModelRc::new(VecModel::from(included_model)));
add_included_directories(&settings, &folders);
} else {
let excluded_model = settings.get_excluded_directories();
let mut excluded_model = excluded_model.iter().collect::<Vec<_>>();
excluded_model.extend(non_empty_lines.iter().map(|x| {
let mut element = slint::StandardListViewItem::default();
element.text = (*x).into();
element
}));
excluded_model.sort_by_cached_key(|x| x.text.to_string());
excluded_model.dedup();
settings.set_excluded_directories(ModelRc::new(VecModel::from(excluded_model)));
add_excluded_directories(&settings, &folders);
}
});
}
fn connect_remove_directories(app: &MainWindow) {
let a = app.as_weak();
app.global::<Callabler>().on_remove_item_directories(move |included_directories, current_index| {
app.global::<Callabler>().on_remove_item_directories(move |included_directories| {
let app = a.upgrade().unwrap();
let settings = app.global::<Settings>();
let current_index = if included_directories {
settings.get_included_directories_model_selected_idx()
} else {
settings.get_excluded_directories_model_selected_idx()
};
// Nothing selected
if current_index == -1 {
return;
}
let app = a.upgrade().unwrap();
let settings = app.global::<Settings>();
if included_directories {
let included_model = settings.get_included_directories();
let included_model = settings.get_included_directories_model();
let model_count = included_model.iter().count();
if model_count > current_index as usize {
let mut included_model = included_model.iter().collect::<Vec<_>>();
included_model.remove(current_index as usize);
settings.set_included_directories(ModelRc::new(VecModel::from(included_model)));
if current_index as usize != model_count - 1 {
included_model[current_index as usize].selected_row = true;
settings.set_included_directories_model_selected_idx(current_index);
} else {
settings.set_included_directories_model_selected_idx(-1);
}
settings.set_included_directories_model(ModelRc::new(VecModel::from(included_model)));
}
} else {
let excluded_model = settings.get_excluded_directories();
let excluded_model = settings.get_excluded_directories_model();
let model_count = excluded_model.iter().count();
if model_count > current_index as usize {
let mut excluded_model = excluded_model.iter().collect::<Vec<_>>();
excluded_model.remove(current_index as usize);
settings.set_excluded_directories(ModelRc::new(VecModel::from(excluded_model)));
if current_index as usize != model_count - 1 {
excluded_model[current_index as usize].selected_row = true;
settings.set_excluded_directories_model_selected_idx(current_index);
} else {
settings.set_excluded_directories_model_selected_idx(-1);
}
settings.set_excluded_directories_model(ModelRc::new(VecModel::from(excluded_model)));
}
}
});
@ -89,33 +90,54 @@ fn connect_add_directories(app: &MainWindow) {
let Some(folders) = file_dialog.pick_folders() else {
return;
};
let folders = folders.iter().map(|x| x.to_string_lossy().to_string()).collect::<Vec<_>>();
let settings = app.global::<Settings>();
let old_folders = if included_directories {
settings.get_included_directories()
} else {
settings.get_excluded_directories()
};
let mut new_folders = old_folders.iter().map(|x| x.text.to_string()).collect::<Vec<_>>();
new_folders.extend(folders.iter().map(|x| x.to_string_lossy().to_string()));
new_folders.sort();
new_folders.dedup();
let new_folders_standard_list_view = new_folders
.iter()
.map(|x| {
let mut element = slint::StandardListViewItem::default();
element.text = x.into();
element
})
.collect::<Vec<_>>();
let new_folders_model = ModelRc::new(VecModel::from(new_folders_standard_list_view));
if included_directories {
settings.set_included_directories(new_folders_model);
add_included_directories(&settings, &folders);
} else {
settings.set_excluded_directories(new_folders_model);
add_excluded_directories(&settings, &folders);
}
});
}
fn add_included_directories(settings: &Settings, folders: &[String]) {
let old_folders = settings.get_included_directories_model();
let old_folders_path = old_folders.iter().map(|x| x.path.to_string()).collect::<Vec<_>>();
let mut new_folders = old_folders.iter().collect::<Vec<_>>();
let filtered_folders = folders.iter().filter(|x| !old_folders_path.contains(x)).collect::<Vec<_>>();
new_folders.iter_mut().for_each(|x| x.selected_row = false);
new_folders.extend(filtered_folders.iter().map(|path| IncludedDirectoriesModel {
path: (*path).into(),
referenced_folder: false,
selected_row: false,
}));
new_folders.sort_by_key(|x| x.path.clone());
let new_folders_model = ModelRc::new(VecModel::from(new_folders));
settings.set_included_directories_model(new_folders_model);
}
fn add_excluded_directories(settings: &Settings, folders: &[String]) {
let old_folders = settings.get_excluded_directories_model();
let old_folders_path = old_folders.iter().map(|x| x.path.to_string()).collect::<Vec<_>>();
let mut new_folders = old_folders.iter().collect::<Vec<_>>();
let filtered_folders = folders.iter().filter(|x| !old_folders_path.contains(x)).collect::<Vec<_>>();
new_folders.iter_mut().for_each(|x| x.selected_row = false);
new_folders.extend(filtered_folders.iter().map(|path| ExcludedDirectoriesModel {
path: (*path).into(),
selected_row: false,
}));
new_folders.sort_by_key(|x| x.path.clone());
let new_folders_model = ModelRc::new(VecModel::from(new_folders));
settings.set_excluded_directories_model(new_folders_model);
}

122
krokiet/src/connect_move.rs Normal file
View file

@ -0,0 +1,122 @@
use crate::common::{get_is_header_mode, get_tool_model, set_tool_model};
use crate::model_operations::{collect_path_name_from_model, deselect_all_items, filter_out_checked_items};
use crate::{Callabler, CurrentTab, GuiState, MainListModel, MainWindow};
use czkawka_core::common_messages::Messages;
use rayon::prelude::*;
use rfd::FileDialog;
use slint::{ComponentHandle, ModelRc, VecModel};
use std::path::{Path, PathBuf};
use std::{fs, path};
pub fn connect_move(app: &MainWindow) {
let a = app.as_weak();
app.on_folders_move_choose_requested(move || {
let app = a.upgrade().unwrap();
let file_dialog = FileDialog::new();
let Some(folder) = file_dialog.pick_folder() else {
return;
};
let folder_str = folder.to_string_lossy().to_string();
app.invoke_show_move_folders_dialog(folder_str.into());
});
let a = app.as_weak();
app.global::<Callabler>().on_move_items(move |preserve_structure, copy_mode, output_folder| {
let app = a.upgrade().unwrap();
let active_tab = app.global::<GuiState>().get_active_tab();
let current_model = get_tool_model(&app, active_tab);
let (errors, new_model) = move_operation(&current_model, preserve_structure, copy_mode, &output_folder, active_tab);
if let Some(new_model) = new_model {
set_tool_model(&app, active_tab, new_model);
}
app.global::<GuiState>().set_info_text(Messages::new_from_errors(errors).create_messages_text().into());
});
}
fn move_operation(
items: &ModelRc<MainListModel>,
preserve_structure: bool,
copy_mode: bool,
output_folder: &str,
active_tab: CurrentTab,
) -> (Vec<String>, Option<ModelRc<MainListModel>>) {
let (entries_to_move, mut entries_left) = filter_out_checked_items(items, get_is_header_mode(active_tab));
if !entries_to_move.is_empty() {
let vec_items_to_move = collect_path_name_from_model(&entries_to_move, active_tab);
let errors = move_selected_items(vec_items_to_move, preserve_structure, copy_mode, output_folder);
deselect_all_items(&mut entries_left);
let r = ModelRc::new(VecModel::from(entries_left));
return (errors, Some(r));
}
(vec![], None)
}
fn move_selected_items(items_to_move: Vec<(String, String)>, preserve_structure: bool, copy_mode: bool, output_folder: &str) -> Vec<String> {
if let Err(err) = fs::create_dir_all(output_folder) {
return vec![format!("Error while creating folder: {err}")];
}
if copy_mode {
items_to_move
.into_par_iter()
.filter_map(|(path, name)| {
let (input_file, output_file) = collect_path_and_create_folders(&path, &name, output_folder, preserve_structure);
if let Err(e) = fs::copy(&input_file, &output_file) {
return Some(format!("Error while copying file {input_file:?} to {output_file:?}, reason {e}"));
}
None
})
.collect()
} else {
items_to_move
.into_par_iter()
.filter_map(|(path, name)| {
let (input_file, output_file) = collect_path_and_create_folders(&path, &name, output_folder, preserve_structure);
if output_file.exists() {
return Some(format!("File {output_file:?} already exists"));
}
// Try to rename file, may fail due various reasons
if fs::rename(&input_file, &output_file).is_ok() {
return None;
}
// It is possible that this failed, because file is on different partition, so
// we need to copy file and then remove old
if let Err(e) = fs::copy(&input_file, &output_file) {
return Some(format!(
"Error while copying file {input_file:?} to {output_file:?}(moving into different partition), reason {e}"
));
}
if let Err(e) = fs::remove_file(&input_file) {
return Some(format!("Error while removing file {input_file:?}(after copying into different partition), reason {e}"));
}
None
})
.collect()
}
}
// Create input/output paths, and create output folder
fn collect_path_and_create_folders(input_path: &str, input_file: &str, output_path: &str, preserve_structure: bool) -> (PathBuf, PathBuf) {
let mut input_full_path = PathBuf::from(input_path);
input_full_path.push(input_file);
let mut output_full_path = PathBuf::from(output_path);
if preserve_structure {
output_full_path.extend(Path::new(input_path).components().filter(|c| matches!(c, path::Component::Normal(_))));
};
let _ = fs::create_dir_all(&output_full_path);
output_full_path.push(input_file);
(input_full_path, output_full_path)
}

View file

@ -1,8 +1,9 @@
use crate::{Callabler, MainWindow};
use directories_next::ProjectDirs;
use log::error;
use slint::ComponentHandle;
use crate::{Callabler, MainWindow};
pub fn connect_open_items(app: &MainWindow) {
app.global::<Callabler>().on_item_opened(move |path| {
match open::that(&*path) {

View file

@ -1,9 +1,11 @@
use crate::{MainWindow, ProgressToSend};
use std::thread;
use crossbeam_channel::Receiver;
use czkawka_core::common_dir_traversal::{ProgressData, ToolType};
use slint::ComponentHandle;
use std::thread;
use czkawka_core::common_dir_traversal::{ProgressData, ToolType};
use crate::{MainWindow, ProgressToSend};
pub fn connect_progress_gathering(app: &MainWindow, progress_receiver: Receiver<ProgressData>) {
let a = app.as_weak();

View file

@ -1,7 +1,12 @@
use crate::settings::{collect_settings, SettingsCustom, ALLOWED_HASH_TYPE_VALUES, ALLOWED_RESIZE_ALGORITHM_VALUES};
use crate::{CurrentTab, GuiState, MainListModel, MainWindow, ProgressToSend};
use std::rc::Rc;
use std::thread;
use chrono::NaiveDateTime;
use crossbeam_channel::{Receiver, Sender};
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_tool::CommonData;
@ -9,12 +14,11 @@ use czkawka_core::common_traits::ResultEntry;
use czkawka_core::empty_files::EmptyFiles;
use czkawka_core::empty_folder::{EmptyFolder, FolderEntry};
use czkawka_core::similar_images;
use czkawka_core::similar_images::SimilarImages;
use humansize::{format_size, BINARY};
use rayon::prelude::*;
use slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};
use std::rc::Rc;
use std::thread;
use czkawka_core::similar_images::{ImagesEntry, SimilarImages};
use crate::common::split_u64_into_i32s;
use crate::settings::{collect_settings, SettingsCustom, ALLOWED_HASH_TYPE_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<()>) {
let a = app.as_weak();
@ -47,7 +51,6 @@ pub fn connect_scan_button(app: &MainWindow, progress_sender: Sender<ProgressDat
});
}
// TODO handle referenced folders
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)
@ -67,49 +70,88 @@ fn scan_similar_images(a: Weak<MainWindow>, progress_sender: Sender<ProgressData
.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));
let mut vector = finder.get_similar_images().clone();
let messages = finder.get_text_messages().create_messages_text();
if finder.get_use_reference() {
let mut vector = finder.get_similar_images_referenced().clone();
let messages = finder.get_text_messages().create_messages_text();
for vec_fe in &mut vector {
vec_fe.par_sort_unstable_by_key(|e| e.similarity);
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);
})
}
let hash_size = custom_settings.similar_images_sub_hash_size;
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<Vec<similar_images::ImagesEntry>>, messages: String, hash_size: u8) {
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 vec_fe in vector {
insert_data_to_model(&items, ModelRc::new(VecModel::default()), true);
for fe in vec_fe {
let (directory, file) = split_path(fe.get_path());
let data_model = VecModel::from_slice(&[
similar_images::get_string_from_similarity(&fe.similarity, hash_size).into(),
format_size(fe.size, BINARY).into(),
fe.dimensions.clone().into(),
file.into(),
directory.into(),
NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(),
]);
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);
insert_data_to_model(&items, data_model, 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, 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)
@ -133,20 +175,28 @@ fn write_empty_files_results(app: &MainWindow, vector: Vec<FileEntry>, messages:
let items_found = vector.len();
let items = Rc::new(VecModel::default());
for fe in vector {
let (directory, file) = split_path(fe.get_path());
let data_model = VecModel::from_slice(&[
file.into(),
directory.into(),
NaiveDateTime::from_timestamp_opt(fe.get_modified_date() as i64, 0).unwrap().to_string().into(),
]);
insert_data_to_model(&items, data_model, false);
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);
}
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)
}
////////////////////////////////////////// Empty Folders
fn scan_empty_folders(a: Weak<MainWindow>, progress_sender: Sender<ProgressData>, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) {
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
@ -170,26 +220,34 @@ fn write_empty_folders_results(app: &MainWindow, vector: Vec<FolderEntry>, messa
let items_found = vector.len();
let items = Rc::new(VecModel::default());
for fe in vector {
let (directory, file) = split_path(&fe.path);
let data_model = VecModel::from_slice(&[
file.into(),
directory.into(),
NaiveDateTime::from_timestamp_opt(fe.modified_date as i64, 0).unwrap().to_string().into(),
]);
insert_data_to_model(&items, data_model, false);
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);
}
app.set_empty_folder_model(items.into());
app.invoke_scan_ended(format!("Found {items_found} empty folders").into());
app.global::<GuiState>().set_info_text(messages.into());
}
fn insert_data_to_model(items: &Rc<VecModel<MainListModel>>, data_model: ModelRc<SharedString>, header_row: bool) {
fn prepare_data_model_empty_folders(fe: &FolderEntry) -> (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)
}
////////////////////////////////////////// Common
fn insert_data_to_model(items: &Rc<VecModel<MainListModel>>, data_model_str: ModelRc<SharedString>, data_model_int: ModelRc<i32>, header_row: bool) {
let main = MainListModel {
checked: false,
header_row,
selected_row: false,
val: ModelRc::new(data_model),
val_str: ModelRc::new(data_model_str),
val_int: ModelRc::new(data_model_int),
};
items.push(main);
}
@ -199,11 +257,13 @@ where
T: CommonData,
{
component.set_included_directory(custom_settings.included_directories.clone());
component.set_reference_directory(custom_settings.included_directories_referenced.clone());
component.set_excluded_directory(custom_settings.excluded_directories.clone());
component.set_recursive_search(custom_settings.recursive_search);
component.set_minimal_file_size(custom_settings.minimum_file_size as u64 * 1024);
component.set_maximal_file_size(custom_settings.maximum_file_size as u64 * 1024);
component.set_allowed_extensions(custom_settings.allowed_extensions.clone());
component.set_excluded_extensions(custom_settings.excluded_extensions.clone());
component.set_excluded_items(custom_settings.excluded_items.split(',').map(str::to_string).collect());
component.set_exclude_other_filesystems(custom_settings.ignore_other_file_systems);
component.set_use_cache(custom_settings.use_cache);

View file

@ -0,0 +1,216 @@
use crate::common::{
connect_i32_into_u64, get_int_height_idx, get_int_modification_date_idx, get_int_size_idx, get_int_width_idx, get_is_header_mode, get_tool_model, set_tool_model,
};
use crate::{Callabler, CurrentTab, GuiState, MainListModel, MainWindow, SelectMode, SelectModel};
use slint::{ComponentHandle, Model, ModelRc, VecModel};
// TODO optimize this, not sure if it is possible to not copy entire model to just select item
// https://github.com/slint-ui/slint/discussions/4595
pub fn connect_select(app: &MainWindow) {
let a = app.as_weak();
app.global::<Callabler>().on_select_items(move |select_mode| {
let app = a.upgrade().unwrap();
let active_tab = app.global::<GuiState>().get_active_tab();
let current_model = get_tool_model(&app, active_tab);
let new_model = match select_mode {
SelectMode::SelectAll => select_all(current_model),
SelectMode::UnselectAll => deselect_all(current_model),
SelectMode::InvertSelection => invert_selection(current_model),
SelectMode::SelectTheBiggestSize => select_by_size_date(current_model, active_tab, true, true),
SelectMode::SelectTheSmallestSize => select_by_size_date(current_model, active_tab, false, true),
SelectMode::SelectTheBiggestResolution => select_by_resolution(current_model, active_tab, true),
SelectMode::SelectTheSmallestResolution => select_by_resolution(current_model, active_tab, false),
SelectMode::SelectNewest => select_by_size_date(current_model, active_tab, true, false),
SelectMode::SelectOldest => select_by_size_date(current_model, active_tab, false, false),
};
set_tool_model(&app, active_tab, new_model);
});
}
pub fn connect_showing_proper_select_buttons(app: &MainWindow) {
set_select_buttons(app);
let a = app.as_weak();
app.global::<Callabler>().on_tab_changed(move || {
let app = a.upgrade().unwrap();
set_select_buttons(&app);
});
}
fn set_select_buttons(app: &MainWindow) {
let active_tab = app.global::<GuiState>().get_active_tab();
let mut base_buttons = vec![SelectMode::SelectAll, SelectMode::UnselectAll, SelectMode::InvertSelection];
let additional_buttons = match active_tab {
CurrentTab::SimilarImages => vec![
SelectMode::SelectOldest,
SelectMode::SelectNewest,
SelectMode::SelectTheSmallestSize,
SelectMode::SelectTheBiggestSize,
SelectMode::SelectTheSmallestResolution,
SelectMode::SelectTheBiggestResolution,
],
_ => vec![],
};
base_buttons.extend(additional_buttons);
base_buttons.reverse();
let new_select_model = base_buttons
.into_iter()
.map(|e| SelectModel {
name: translate_select_mode(e).into(),
data: e,
})
.collect::<Vec<_>>();
app.global::<GuiState>().set_select_results_list(ModelRc::new(VecModel::from(new_select_model)));
}
fn translate_select_mode(select_mode: SelectMode) -> String {
match select_mode {
SelectMode::SelectAll => "Select all".into(),
SelectMode::UnselectAll => "Unselect all".into(),
SelectMode::InvertSelection => "Invert selection".into(),
SelectMode::SelectTheBiggestSize => "Select the biggest size".into(),
SelectMode::SelectTheBiggestResolution => "Select the biggest resolution".into(),
SelectMode::SelectTheSmallestSize => "Select the smallest size".into(),
SelectMode::SelectTheSmallestResolution => "Select the smallest resolution".into(),
SelectMode::SelectNewest => "Select newest".into(),
SelectMode::SelectOldest => "Select oldest".into(),
}
}
// TODO, when model will be able to contain i64 instead two i32, this function could be merged with select_by_size_date
fn select_by_resolution(model: ModelRc<MainListModel>, active_tab: CurrentTab, biggest: bool) -> ModelRc<MainListModel> {
let is_header_mode = get_is_header_mode(active_tab);
assert!(is_header_mode); // non header modes not really have reason to use this function
let mut old_data = model.iter().collect::<Vec<_>>();
let headers_idx = find_header_idx_and_deselect_all(&mut old_data);
let width_idx = get_int_width_idx(active_tab);
let height_idx = get_int_height_idx(active_tab);
if biggest {
for i in 0..(headers_idx.len() - 1) {
let mut max_item = 0;
let mut max_item_idx = 0;
#[allow(clippy::needless_range_loop)]
for j in (headers_idx[i] + 1)..headers_idx[i + 1] {
let int_data = old_data[j].val_int.iter().collect::<Vec<_>>();
let item = int_data[width_idx] * int_data[height_idx];
if item > max_item {
max_item = item;
max_item_idx = j;
}
}
old_data[max_item_idx].checked = true;
}
} else {
for i in 0..(headers_idx.len() - 1) {
let mut min_item = u64::MAX;
let mut min_item_idx = 0;
#[allow(clippy::needless_range_loop)]
for j in (headers_idx[i] + 1)..headers_idx[i + 1] {
let int_data = old_data[j].val_int.iter().collect::<Vec<_>>();
let item = (int_data[width_idx] * int_data[height_idx]) as u64;
if item < min_item {
min_item = item;
min_item_idx = j;
}
}
old_data[min_item_idx].checked = true;
}
}
ModelRc::new(VecModel::from(old_data))
}
fn select_by_size_date(model: ModelRc<MainListModel>, active_tab: CurrentTab, biggest_newest: bool, size: bool) -> ModelRc<MainListModel> {
let is_header_mode = get_is_header_mode(active_tab);
assert!(is_header_mode); // non header modes not really have reason to use this function
let mut old_data = model.iter().collect::<Vec<_>>();
let headers_idx = find_header_idx_and_deselect_all(&mut old_data);
let item_idx = if size {
get_int_size_idx(active_tab)
} else {
get_int_modification_date_idx(active_tab)
};
if biggest_newest {
for i in 0..(headers_idx.len() - 1) {
let mut max_item = 0;
let mut max_item_idx = 0;
#[allow(clippy::needless_range_loop)]
for j in (headers_idx[i] + 1)..headers_idx[i + 1] {
let int_data = old_data[j].val_int.iter().collect::<Vec<_>>();
let item = connect_i32_into_u64(int_data[item_idx], int_data[item_idx + 1]);
if item > max_item {
max_item = item;
max_item_idx = j;
}
}
old_data[max_item_idx].checked = true;
}
} else {
for i in 0..(headers_idx.len() - 1) {
let mut min_item = u64::MAX;
let mut min_item_idx = 0;
#[allow(clippy::needless_range_loop)]
for j in (headers_idx[i] + 1)..headers_idx[i + 1] {
let int_data = old_data[j].val_int.iter().collect::<Vec<_>>();
let item = connect_i32_into_u64(int_data[item_idx], int_data[item_idx + 1]);
if item < min_item {
min_item = item;
min_item_idx = j;
}
}
old_data[min_item_idx].checked = true;
}
}
ModelRc::new(VecModel::from(old_data))
}
fn select_all(model: ModelRc<MainListModel>) -> ModelRc<MainListModel> {
let mut old_data = model.iter().collect::<Vec<_>>();
for x in &mut old_data {
if !x.header_row {
x.checked = true;
}
}
ModelRc::new(VecModel::from(old_data))
}
fn deselect_all(model: ModelRc<MainListModel>) -> ModelRc<MainListModel> {
let mut old_data = model.iter().collect::<Vec<_>>();
old_data.iter_mut().for_each(|x| x.checked = false);
ModelRc::new(VecModel::from(old_data))
}
fn invert_selection(model: ModelRc<MainListModel>) -> ModelRc<MainListModel> {
let mut old_data = model.iter().collect::<Vec<_>>();
for x in &mut old_data {
if !x.header_row {
x.checked = !x.checked;
}
}
ModelRc::new(VecModel::from(old_data))
}
fn find_header_idx_and_deselect_all(old_data: &mut [MainListModel]) -> Vec<usize> {
let mut header_idx = old_data
.iter()
.enumerate()
.filter_map(|(idx, m)| if m.header_row { Some(idx) } else { None })
.collect::<Vec<_>>();
header_idx.push(old_data.len());
for x in old_data.iter_mut() {
if !x.header_row {
x.checked = false;
}
}
header_idx
}

View file

@ -1,10 +1,13 @@
use crate::{Callabler, GuiState, MainWindow};
use czkawka_core::common::{get_dynamic_image_from_raw_image, IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS};
use std::path::Path;
use std::time::{Duration, Instant};
use image::DynamicImage;
use log::{debug, error};
use slint::ComponentHandle;
use std::path::Path;
use std::time::{Duration, Instant};
use czkawka_core::common::{get_dynamic_image_from_raw_image, IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS};
use crate::{Callabler, CurrentTab, GuiState, MainWindow, Settings};
pub type ImageBufferRgba = image::ImageBuffer<image::Rgba<u8>, Vec<u8>>;
@ -13,6 +16,21 @@ pub fn connect_show_preview(app: &MainWindow) {
app.global::<Callabler>().on_load_image_preview(move |image_path| {
let app = a.upgrade().unwrap();
let settings = app.global::<Settings>();
let gui_state = app.global::<GuiState>();
let active_tab = gui_state.get_active_tab();
if active_tab == CurrentTab::SimilarImages && !settings.get_similar_images_show_image_preview() {
set_preview_visible(&gui_state, None);
return;
}
// Do not load the same image again
if image_path == gui_state.get_preview_image_path() {
return;
}
let path = Path::new(image_path.as_str());
let res = load_image(path);
@ -22,20 +40,30 @@ pub fn connect_show_preview(app: &MainWindow) {
let convert_time = start_timer_convert_time.elapsed();
let start_set_time = Instant::now();
app.global::<GuiState>().set_preview_image(slint_image);
gui_state.set_preview_image(slint_image);
let set_time = start_set_time.elapsed();
debug!(
"Loading image took: {:?}, converting image took: {:?}, setting image took: {:?}",
load_time, convert_time, set_time
);
app.global::<GuiState>().set_preview_visible(true);
set_preview_visible(&gui_state, Some(image_path.as_str()));
} else {
app.global::<GuiState>().set_preview_visible(false);
set_preview_visible(&gui_state, None);
}
});
}
fn set_preview_visible(gui_state: &GuiState, preview: Option<&str>) {
if let Some(preview) = preview {
gui_state.set_preview_image_path(preview.into());
gui_state.set_preview_visible(true);
} else {
gui_state.set_preview_image_path("".into());
gui_state.set_preview_visible(false);
}
}
fn convert_into_slint_image(img: DynamicImage) -> slint::Image {
let image_buffer: ImageBufferRgba = img.to_rgba8();
let buffer = slint::SharedPixelBuffer::<slint::Rgba8Pixel>::clone_from_slice(image_buffer.as_raw(), image_buffer.width(), image_buffer.height());

View file

@ -1,6 +1,7 @@
use crate::MainWindow;
use crossbeam_channel::Sender;
use crate::MainWindow;
pub fn connect_stop_button(app: &MainWindow, stop_sender: Sender<()>) {
app.on_scan_stopping(move || {
stop_sender.send(()).unwrap();

View file

@ -1,7 +1,9 @@
use std::collections::HashMap;
use slint::{ComponentHandle, Model};
use crate::localizer_krokiet::LANGUAGE_LOADER_GUI;
use crate::{Callabler, MainWindow};
use slint::{ComponentHandle, Model};
use std::collections::HashMap;
pub fn connect_translations(app: &MainWindow) {
app.global::<Callabler>().on_translate(move |text_to_translate, args| {

View file

@ -17,38 +17,42 @@
#![allow(clippy::items_after_statements)] // Generated code
#![allow(clippy::match_same_arms)] // Generated code
mod common;
mod connect_delete;
mod connect_directories_changes;
mod connect_open;
mod connect_progress_receiver;
mod connect_scan;
mod connect_show_preview;
mod connect_stop;
mod connect_translation;
mod localizer_krokiet;
mod set_initial_gui_info;
mod settings;
use std::rc::Rc;
use crossbeam_channel::{unbounded, Receiver, Sender};
use slint::VecModel;
use std::rc::Rc;
// use std::rc::Rc;
use czkawka_core::common::{print_version_mode, setup_logger};
use czkawka_core::common_dir_traversal::ProgressData;
use crate::connect_delete::connect_delete_button;
use crate::connect_open::connect_open_items;
use crate::connect_scan::connect_scan_button;
use crate::connect_directories_changes::connect_add_remove_directories;
use crate::connect_move::connect_move;
use crate::connect_open::connect_open_items;
use crate::connect_progress_receiver::connect_progress_gathering;
use crate::connect_scan::connect_scan_button;
use crate::connect_select::{connect_select, connect_showing_proper_select_buttons};
use crate::connect_show_preview::connect_show_preview;
use crate::connect_stop::connect_stop_button;
use crate::connect_translation::connect_translations;
use crate::set_initial_gui_info::set_initial_gui_infos;
use crate::settings::{connect_changing_settings_preset, create_default_settings_files, load_settings_from_file, save_all_settings_to_file};
use czkawka_core::common::{print_version_mode, setup_logger};
use czkawka_core::common_dir_traversal::ProgressData;
// use slint::{ModelRc, VecModel};
mod common;
mod connect_delete;
mod connect_directories_changes;
mod connect_move;
mod connect_open;
mod connect_progress_receiver;
mod connect_scan;
mod connect_select;
mod connect_show_preview;
mod connect_stop;
mod connect_translation;
mod localizer_krokiet;
mod model_operations;
mod set_initial_gui_info;
mod settings;
slint::include_modules!();
fn main() {
@ -80,6 +84,9 @@ fn main() {
connect_show_preview(&app);
connect_translations(&app);
connect_changing_settings_preset(&app);
connect_select(&app);
connect_showing_proper_select_buttons(&app);
connect_move(&app);
app.run().unwrap();

View file

@ -0,0 +1,238 @@
use crate::common::{get_str_name_idx, get_str_path_idx};
use crate::{CurrentTab, MainListModel};
use slint::{Model, ModelRc};
use std::path::MAIN_SEPARATOR;
pub fn deselect_all_items(items: &mut [MainListModel]) {
for item in items {
item.selected_row = false;
}
}
#[allow(unused)]
pub fn select_all_items(items: &mut [MainListModel]) {
for item in items {
item.selected_row = true;
}
}
pub fn collect_full_path_from_model(items: &[MainListModel], active_tab: CurrentTab) -> Vec<String> {
let path_idx = get_str_path_idx(active_tab);
let name_idx = get_str_name_idx(active_tab);
items
.iter()
.map(|item| {
let path = item.val_str.iter().nth(path_idx).unwrap();
let name = item.val_str.iter().nth(name_idx).unwrap();
format!("{}{}{}", path, MAIN_SEPARATOR, name)
})
.collect::<Vec<_>>()
}
pub fn collect_path_name_from_model(items: &[MainListModel], active_tab: CurrentTab) -> Vec<(String, String)> {
let path_idx = get_str_path_idx(active_tab);
let name_idx = get_str_name_idx(active_tab);
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(),
)
})
.collect::<Vec<_>>()
}
pub fn filter_out_checked_items(items: &ModelRc<MainListModel>, have_header: bool) -> (Vec<MainListModel>, Vec<MainListModel>) {
if cfg!(debug_assertions) {
check_if_header_is_checked(items);
check_if_header_is_selected_but_should_not_be(items, have_header);
}
let (entries_to_delete, mut entries_left): (Vec<_>, Vec<_>) = items.iter().partition(|item| item.checked);
// When have header, we must also throw out orphaned items - this needs to be
if have_header && !entries_left.is_empty() {
// First row must be header
assert!(entries_left[0].header_row);
if entries_left.len() == 3 {
// First row is header, so if second or third is also header, then there is no enough items to fill model
if entries_left[1].header_row || entries_left[2].header_row {
entries_left = Vec::new();
}
} else if entries_left.len() < 3 {
// Not have enough items to fill model
entries_left = Vec::new();
} else {
let mut last_header = 0;
let mut new_items: Vec<MainListModel> = Vec::new();
for i in 1..entries_left.len() {
if entries_left[i].header_row {
if i - last_header > 2 {
new_items.extend(entries_left[last_header..i].iter().cloned());
}
last_header = i;
}
}
if entries_left.len() - last_header > 2 {
new_items.extend(entries_left[last_header..].iter().cloned());
}
entries_left = new_items;
}
}
(entries_to_delete, entries_left)
}
// Function to verify if really headers are not checked
// Checked header is big bug
fn check_if_header_is_checked(items: &ModelRc<MainListModel>) {
if cfg!(debug_assertions) {
for item in items.iter() {
if item.header_row {
assert!(!item.checked);
}
}
}
}
// In some modes header should not be visible, but if are, then it is a bug
fn check_if_header_is_selected_but_should_not_be(items: &ModelRc<MainListModel>, can_have_header: bool) {
if cfg!(debug_assertions) {
if !can_have_header {
for item in items.iter() {
assert!(!item.header_row);
}
}
}
}
#[cfg(test)]
mod tests {
use slint::{Model, ModelRc, SharedString, VecModel};
use crate::model_operations::filter_out_checked_items;
use crate::MainListModel;
#[test]
fn test_filter_out_checked_items_empty() {
let items: ModelRc<MainListModel> = create_new_model(vec![]);
let (to_delete, left) = filter_out_checked_items(&items, false);
assert!(to_delete.is_empty());
assert!(left.is_empty());
let (to_delete, left) = filter_out_checked_items(&items, true);
assert!(to_delete.is_empty());
assert!(left.is_empty());
}
#[test]
fn test_filter_out_checked_items_one_element_valid_normal() {
let items = create_new_model(vec![(false, false, false, vec![])]);
let (to_delete, left) = filter_out_checked_items(&items, false);
assert!(to_delete.is_empty());
assert_eq!(left.len(), items.iter().count());
}
#[test]
fn test_filter_out_checked_items_one_element_valid_header() {
let items = create_new_model(vec![(false, true, false, vec![])]);
let (to_delete, left) = filter_out_checked_items(&items, true);
assert!(to_delete.is_empty());
assert!(left.is_empty());
}
#[test]
#[should_panic]
fn test_filter_out_checked_items_one_element_invalid_normal() {
let items = create_new_model(vec![(false, true, false, vec![])]);
filter_out_checked_items(&items, false);
}
#[test]
#[should_panic]
fn test_filter_out_checked_items_one_element_invalid_header() {
let items = create_new_model(vec![(false, false, false, vec![])]);
filter_out_checked_items(&items, true);
}
#[test]
fn test_filter_out_checked_items_multiple_element_valid_normal() {
let items = create_new_model(vec![
(false, false, false, vec!["1"]),
(false, false, false, vec!["2"]),
(true, false, false, vec!["3"]),
(true, false, false, vec!["4"]),
(false, false, false, vec!["5"]),
]);
let (to_delete, left) = filter_out_checked_items(&items, false);
let to_delete_data = get_single_data_str_from_model(&to_delete);
let left_data = get_single_data_str_from_model(&left);
assert_eq!(to_delete_data, vec!["3", "4"]);
assert_eq!(left_data, vec!["1", "2", "5"]);
}
#[test]
fn test_filter_out_checked_items_multiple_element_valid_header() {
let items = create_new_model(vec![
(false, true, false, vec!["1"]),
(false, false, false, vec!["2"]),
(true, false, false, vec!["3"]),
(false, true, false, vec!["4"]),
(false, false, false, vec!["5"]),
(false, true, false, vec!["6"]),
(false, false, false, vec!["7"]),
(false, false, false, vec!["8"]),
]);
let (to_delete, left) = filter_out_checked_items(&items, true);
let to_delete_data = get_single_data_str_from_model(&to_delete);
let left_data = get_single_data_str_from_model(&left);
assert_eq!(to_delete_data, vec!["3"]);
assert_eq!(left_data, vec!["6", "7", "8"]);
}
#[test]
fn test_filter_out_checked_items_multiple2_element_valid_header() {
let items = create_new_model(vec![
(false, true, false, vec!["1"]),
(false, false, false, vec!["2"]),
(true, false, false, vec!["3"]),
(false, false, false, vec!["4"]),
(false, false, false, vec!["5"]),
(false, false, false, vec!["6"]),
(false, true, false, vec!["7"]),
(false, false, false, vec!["8"]),
]);
let (to_delete, left) = filter_out_checked_items(&items, true);
let to_delete_data = get_single_data_str_from_model(&to_delete);
let left_data = get_single_data_str_from_model(&left);
assert_eq!(to_delete_data, vec!["3"]);
assert_eq!(left_data, vec!["1", "2", "4", "5", "6"]);
}
fn get_single_data_str_from_model(model: &[MainListModel]) -> Vec<String> {
let mut d = model.iter().map(|item| item.val_str.iter().next().unwrap().to_string()).collect::<Vec<_>>();
d.sort();
d
}
fn create_new_model(items: Vec<(bool, bool, bool, Vec<&'static str>)>) -> ModelRc<MainListModel> {
let model = VecModel::default();
for item in items {
let all_items: Vec<SharedString> = item.3.iter().map(|item| (*item).into()).collect::<Vec<_>>();
let all_items = VecModel::from(all_items);
model.push(MainListModel {
checked: item.0,
header_row: item.1,
selected_row: item.2,
val_str: ModelRc::new(all_items),
val_int: ModelRc::new(VecModel::default()),
});
}
ModelRc::new(model)
}
}

View file

@ -1,13 +1,14 @@
use czkawka_core::common::get_available_threads;
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::{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_available_threads();
let threads = get_all_available_threads();
let settings = app.global::<Settings>();
app.global::<GuiState>().set_maximum_threads(threads as f32);

View file

@ -9,10 +9,10 @@ use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use slint::{ComponentHandle, Model, ModelRc};
use czkawka_core::common::{get_available_threads, set_number_of_threads};
use czkawka_core::common::{get_all_available_threads, set_number_of_threads};
use czkawka_core::common_items::{DEFAULT_EXCLUDED_DIRECTORIES, DEFAULT_EXCLUDED_ITEMS};
use crate::common::{create_string_standard_list_view_from_pathbuf, create_vec_model_from_vec_string};
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};
pub const DEFAULT_MINIMUM_SIZE_KB: i32 = 16;
@ -44,12 +44,16 @@ pub const ALLOWED_HASH_TYPE_VALUES: &[(&str, &str, HashAlg)] = &[
pub struct SettingsCustom {
#[serde(default = "default_included_directories")]
pub included_directories: Vec<PathBuf>,
#[serde(default)]
pub included_directories_referenced: Vec<PathBuf>,
#[serde(default = "default_excluded_directories")]
pub excluded_directories: Vec<PathBuf>,
#[serde(default = "default_excluded_items")]
pub excluded_items: String,
#[serde(default)]
pub allowed_extensions: String,
#[serde(default)]
pub excluded_extensions: String,
#[serde(default = "minimum_file_size")]
pub minimum_file_size: i32,
#[serde(default = "maximum_file_size")]
@ -236,7 +240,7 @@ pub fn load_settings_from_file(app: &MainWindow) {
}
}
base_settings.default_preset = max(min(base_settings.default_preset, 9), 0);
custom_settings.thread_number = max(min(custom_settings.thread_number, get_available_threads() as i32), 0);
custom_settings.thread_number = max(min(custom_settings.thread_number, get_all_available_threads() as i32), 0);
// Ended validating
set_settings_to_gui(app, &custom_settings);
@ -259,7 +263,7 @@ pub fn save_base_settings_to_file(app: &MainWindow) {
pub fn save_custom_settings_to_file(app: &MainWindow) {
let current_item = app.global::<Settings>().get_settings_preset_idx();
let result = save_data_to_file(get_config_file(current_item + 1), &collect_settings(app));
let result = save_data_to_file(get_config_file(current_item), &collect_settings(app));
if let Err(e) = result {
error!("{e}");
@ -348,15 +352,16 @@ pub fn set_settings_to_gui(app: &MainWindow, custom_settings: &SettingsCustom) {
let settings = app.global::<Settings>();
// Included directories
let included_directories = create_string_standard_list_view_from_pathbuf(&custom_settings.included_directories);
settings.set_included_directories(included_directories);
let included_directories = create_included_directories_model_from_pathbuf(&custom_settings.included_directories, &custom_settings.included_directories_referenced);
settings.set_included_directories_model(included_directories);
// Excluded directories
let excluded_directories = create_string_standard_list_view_from_pathbuf(&custom_settings.excluded_directories);
settings.set_excluded_directories(excluded_directories);
let excluded_directories = create_excluded_directories_model_from_pathbuf(&custom_settings.excluded_directories);
settings.set_excluded_directories_model(excluded_directories);
settings.set_excluded_items(custom_settings.excluded_items.clone().into());
settings.set_allowed_extensions(custom_settings.allowed_extensions.clone().into());
settings.set_excluded_extensions(custom_settings.excluded_extensions.clone().into());
settings.set_minimum_file_size(custom_settings.minimum_file_size.to_string().into());
settings.set_maximum_file_size(custom_settings.maximum_file_size.to_string().into());
settings.set_use_cache(custom_settings.use_cache);
@ -430,14 +435,20 @@ pub fn set_settings_to_gui(app: &MainWindow, custom_settings: &SettingsCustom) {
pub fn collect_settings(app: &MainWindow) -> SettingsCustom {
let settings = app.global::<Settings>();
let included_directories = settings.get_included_directories();
let included_directories = included_directories.iter().map(|x| PathBuf::from(x.text.as_str())).collect::<Vec<_>>();
let included_directories_model = settings.get_included_directories_model();
let included_directories = included_directories_model.iter().map(|model| PathBuf::from(model.path.as_str())).collect::<Vec<_>>();
let included_directories_referenced = included_directories_model
.iter()
.filter(|model| model.referenced_folder)
.map(|model| PathBuf::from(model.path.as_str()))
.collect::<Vec<_>>();
let excluded_directories = settings.get_excluded_directories();
let excluded_directories = excluded_directories.iter().map(|x| PathBuf::from(x.text.as_str())).collect::<Vec<_>>();
let excluded_directories_model = settings.get_excluded_directories_model();
let excluded_directories = excluded_directories_model.iter().map(|model| PathBuf::from(model.path.as_str())).collect::<Vec<_>>();
let excluded_items = settings.get_excluded_items().to_string();
let allowed_extensions = settings.get_allowed_extensions().to_string();
let excluded_extensions = settings.get_excluded_extensions().to_string();
let minimum_file_size = settings.get_minimum_file_size().parse::<i32>().unwrap_or(DEFAULT_MINIMUM_SIZE_KB);
let maximum_file_size = settings.get_maximum_file_size().parse::<i32>().unwrap_or(DEFAULT_MAXIMUM_SIZE_KB);
@ -478,9 +489,11 @@ pub fn collect_settings(app: &MainWindow) -> SettingsCustom {
let similar_images_sub_similarity = settings.get_similar_images_sub_current_similarity().round() as i32;
SettingsCustom {
included_directories,
included_directories_referenced,
excluded_directories,
excluded_items,
allowed_extensions,
excluded_extensions,
minimum_file_size,
maximum_file_size,
recursive_search,

View file

@ -20,11 +20,13 @@ export component VisibilityButton inherits Button {
export component ActionButtons inherits HorizontalLayout {
callback scan_stopping;
callback scan_starting(CurrentTab);
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 <bool> stop_requested: false;
in-out property <bool> scanning;
in-out property <bool> lists_enabled: GuiState.active_tab != CurrentTab.Settings;
// in-out property <>
out property <int> name;
height: 30px;
spacing: 4px;
@ -57,35 +59,30 @@ export component ActionButtons inherits HorizontalLayout {
horizontal-stretch: 0.5;
}
move_button := Button {
height: parent.height;
enabled: !scanning && lists_enabled;
text: "Move";
clicked => {
request_folder_to_move();
}
}
select_button := Button {
height: parent.height;
enabled: !scanning && lists_enabled;
text: "Select";
clicked => {
show_select_popup(self.x + self.width / 2, self.y + parent.y);
}
}
delete_button := Button {
height: parent.height;
enabled: !scanning && lists_enabled;
text: "Delete";
clicked => {
Callabler.delete_selected_items();
}
}
popup_item := PopupWindow {
height: root.height;
width: root.width;
close-on-click: true;
VerticalLayout {
for i[idx] in ["A","B","C"]: Rectangle {
background: red;
}
}
}
select_button := Button {
visible: false;
height: parent.height;
enabled: !scanning && lists_enabled;
text: "Select";
clicked => {
debug("Selected");
popup_item.show();
// Callabler.select_items();
show_remove_popup();
}
}

View file

@ -4,10 +4,13 @@ import {Settings} from "settings.slint";
import {BottomPanelVisibility} from "common.slint";
import {Callabler} from "callabler.slint";
import {GuiState} from "gui_state.slint";
import {IncludedDirectories, ExcludedDirectories} from "included_directories.slint";
component DirectoriesPanel inherits HorizontalLayout {
callback folder_choose_requested(bool);
callback show_manual_add_dialog(bool);
spacing: 5px;
// Included directories
VerticalLayout {
horizontal-stretch: 0.0;
@ -22,7 +25,7 @@ component DirectoriesPanel inherits HorizontalLayout {
Button {
text: "Remove";
clicked => {
Callabler.remove_item_directories(true, included-list.current-item);
Callabler.remove_item_directories(true);
}
}
@ -46,9 +49,7 @@ component DirectoriesPanel inherits HorizontalLayout {
}
}
included_list := StandardListView {
model: Settings.included-directories;
}
included_list := IncludedDirectories { }
}
// Excluded directories
@ -65,7 +66,7 @@ component DirectoriesPanel inherits HorizontalLayout {
Button {
text: "Remove";
clicked => {
Callabler.remove_item_directories(false, excluded-list.current-item);
Callabler.remove_item_directories(false);
}
}
@ -89,9 +90,7 @@ component DirectoriesPanel inherits HorizontalLayout {
}
}
excluded_list := StandardListView {
model: Settings.excluded-directories;
}
excluded_list := ExcludedDirectories { }
}
}

View file

@ -1,13 +1,15 @@
import { SelectMode } from "common.slint";
export global Callabler {
// Bottom panel operations
callback remove_item_directories(bool, int);
callback remove_item_directories(bool);
callback added_manual_directories(bool, string);
// Right click or middle click opener
callback item_opened(string);
callback delete_selected_items();
// callback ();
callback select_items(SelectMode);
// Preview
callback load_image_preview(string);
@ -18,6 +20,10 @@ export global Callabler {
callback load_current_preset();
callback reset_current_preset();
callback tab_changed();
callback move_items(bool, bool, string);
// Translations
pure callback translate(string, [{key: string, value: string}]) -> string;

View file

@ -20,11 +20,40 @@ export struct MainListModel {
checked: bool,
header_row: bool,
selected_row: bool,
val: [string]
val_str: [string],
val_int: [int]
}
export enum BottomPanelVisibility {
NotVisible,
TextErrors,
Directories
}
export struct IncludedDirectoriesModel {
path: string,
referenced_folder: bool,
selected_row: bool,
}
export struct ExcludedDirectoriesModel {
path: string,
selected_row: bool,
}
export enum SelectMode {
SelectAll,
UnselectAll,
InvertSelection,
SelectTheBiggestSize,
SelectTheBiggestResolution,
SelectTheSmallestSize,
SelectTheSmallestResolution,
SelectNewest,
SelectOldest,
}
export struct SelectModel {
data: SelectMode,
name: string
}

View file

@ -1,6 +1,8 @@
import {CurrentTab} from "common.slint";
import {SelectModel, SelectMode} from "common.slint";
// State to show
// 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
export global GuiState {
in-out property <length> app_width;
in-out property <length> app_height;
@ -8,6 +10,7 @@ export global GuiState {
in-out property <string> info_text: "Nothing to report";
in-out property <bool> preview_visible;
in-out property <image> preview_image;
in-out property <string> preview_image_path;
in-out property <float> maximum_threads: 40;
@ -16,4 +19,5 @@ export global GuiState {
in-out property <bool> available_subsettings: active_tab == CurrentTab.SimilarImages;
in-out property <CurrentTab> active_tab: CurrentTab.EmptyFiles;
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"}];
}

View file

@ -1,25 +1,24 @@
import {Button, StandardListView, VerticalBox, ListView, ScrollView, TextEdit, CheckBox} from "std-widgets.slint";
import {Callabler} from "callabler.slint";
import {IncludedDirectoriesModel, ExcludedDirectoriesModel} from "common.slint";
import {ColorPalette} from "color_palette.slint";
import {Settings} from "settings.slint";
export struct IncludedDirectoriesModel {
path: string,
referended_folder: bool,
}
export component IncludedDirectories {
in-out property <[IncludedDirectoriesModel]> model <=> Settings.included_directories_model;
in-out property <int> current_index <=> Settings.included_directories_model_selected_idx;
export component InlcudedDirectories {
in-out property <[IncludedDirectoriesModel]> model: [{path: "/home/path", referended_folder: false}];
in-out property <int> current_index: -1;
in-out property <length> size_referenced_folder: 40px;
in-out property <length> size_referenced_folder: 33px;
min-width: 50px;
VerticalLayout {
HorizontalLayout {
spacing: 5px;
Text {
text: "Referenced folder";
text: "Ref";
width: size_referenced_folder;
horizontal-alignment: center;
}
Text{
horizontal-stretch: 1.0;
@ -27,61 +26,26 @@ export component InlcudedDirectories {
}
}
ListView {
for data in model : Rectangle {
height: 30px;
border_radius: 5px;
width: parent.width;
HorizontalLayout {
spacing: 5px;
width: parent.width;
CheckBox {
checked: data.referended_folder;
width: size_referenced_folder;
}
Text {
horizontal-stretch: 1.0;
text: data.path;
vertical-alignment: center;
}
}
}
}
}
}
export component ExcludeDirectories {
in-out property <[string]> model: ["/home/path"];
in-out property <int> current_index: -1;
private property <PointerEvent> event;
min-width: 50px;
VerticalLayout {
HorizontalLayout {
spacing: 5px;
Text {
text: "Path";
}
}
ListView {
for data[idx] in model : Rectangle {
for r[idx] in model : Rectangle {
height: 30px;
border_radius: 5px;
width: parent.width;
background: touch-area.has-hover ? (r.selected_row ? ColorPalette.list-view-normal-selected-header : ColorPalette.list_view_normal_color) : (r.selected_row ? ColorPalette.list-view-normal-selected-header : ColorPalette.list_view_normal_color);
touch_area := TouchArea {
clicked => {
if (current_index == -1) {
r.selected_row = true;
} else {
if (current_index != idx) {
model[current_index].selected_row = false;
}
r.selected_row = true;
}
current_index = idx;
}
double-clicked => {
if (event.button == PointerEventButton.middle && event.kind == PointerEventKind.up) {
Callabler.item_opened(data)
}
}
pointer-event(event) => {
root.event = event;
Callabler.item_opened(r.path);
}
}
@ -89,9 +53,16 @@ export component ExcludeDirectories {
spacing: 5px;
width: parent.width;
CheckBox {
checked: r.referenced_folder;
toggled => {
model[idx].referenced_folder = self.checked;
}
width: size_referenced_folder;
}
Text {
horizontal-stretch: 1.0;
text: data;
text: r.path;
vertical-alignment: center;
}
}
@ -99,3 +70,53 @@ export component ExcludeDirectories {
}
}
}
export component ExcludedDirectories {
in-out property <[ExcludedDirectoriesModel]> model <=> Settings.excluded_directories_model;
in-out property <int> current_index <=> Settings.excluded_directories_model_selected_idx;
min-width: 50px;
VerticalLayout {
HorizontalLayout {
spacing: 5px;
padding-left: 5px;
Text {
text: "Path";
}
}
ListView {
for r[idx] in model : Rectangle {
height: 30px;
border_radius: 5px;
width: parent.width;
background: touch-area.has-hover ? (r.selected_row ? ColorPalette.list-view-normal-selected-header : ColorPalette.list_view_normal_color) : (r.selected_row ? ColorPalette.list-view-normal-selected-header : ColorPalette.list_view_normal_color);
touch_area := TouchArea {
clicked => {
if (current_index == -1) {
r.selected_row = true;
} else {
if (current_index != idx) {
model[current_index].selected_row = false;
}
r.selected_row = true;
}
current_index = idx;
}
double-clicked => {
Callabler.item_opened(r.path);
}
}
Text {
x: 5px;
width: parent.width;
height: parent.height;
text: r.path;
vertical-alignment: center;
}
}
}
}
}

View file

@ -2,6 +2,7 @@ import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListV
import {CurrentTab} from "common.slint";
import {ColorPalette} from "color_palette.slint";
import {GuiState} from "gui_state.slint";
import {Callabler} from "callabler.slint";
component TabItem {
in property <bool> scanning;
@ -19,6 +20,7 @@ component TabItem {
return;
}
GuiState.active_tab = root.curr-tab;
Callabler.tab_changed();
changed_current_tab();
}
}
@ -130,6 +132,7 @@ export component LeftSidePanel {
icon: @image-url("../icons/settings.svg");
clicked => {
GuiState.active_tab = CurrentTab.Settings;
Callabler.tab_changed();
root.changed_current_tab();
}
}

View file

@ -8,10 +8,10 @@ import {GuiState} from "gui_state.slint";
export component MainList {
in-out property <[MainListModel]> empty_folder_model: [
{checked: false, selected_row: false, header_row: true, val: ["kropkarz", "/Xd1", "24.10.2023"]} ,
{checked: false, selected_row: false, header_row: false, val: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"]} ,
{checked: false, selected_row: false, header_row: false, val: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"]} ,
{checked: true, selected_row: false, header_row: false, val: ["lokkaler", "/Xd1/Vide2", "01.23.1911"]}
{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;
in-out property <[MainListModel]> similar_images_model;

View file

@ -13,7 +13,9 @@ 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 { PopupSelect } from "popup_select.slint";
import {PopupDelete} from "popup_delete.slint";
import {PopupMoveFolders} from "popup_move_folders.slint";
import { PopupSelectResults } from "popup_select_results.slint";
import { ToolSettings } from "tool_settings.slint";
export {Settings, Callabler, GuiState}
@ -23,6 +25,8 @@ export component MainWindow inherits Window {
callback scan_starting(CurrentTab);
callback folder_choose_requested(bool);
callback scan_ended(string);
callback show_move_folders_dialog(string);
callback folders_move_choose_requested();
min-width: 300px;
preferred-width: 800px;
@ -38,19 +42,19 @@ export component MainWindow inherits Window {
step_name: "Cache",
};
in-out property <[MainListModel]> empty_folder_model: [
{checked: false, selected_row: false, header_row: true, val: ["kropkarz", "/Xd1", "24.10.2023"]} ,
{checked: false, selected_row: false, header_row: false, val: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"]} ,
{checked: false, selected_row: false, header_row: false, val: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"]} ,
{checked: true, selected_row: false, header_row: false, val: ["lokkaler", "/Xd1/Vide2", "01.23.1911"]}
{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: ["kropkarz", "/Xd1", "24.10.2023"]} ,
{checked: false, selected_row: false, header_row: false, val: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"]} ,
{checked: false, selected_row: false, header_row: false, val: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"]} ,
{checked: true, selected_row: false, header_row: false, val: ["lokkaler", "/Xd1/Vide2", "01.23.1911"]}
{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]> similar_images_model: [];
VerticalBox {
HorizontalBox {
vertical-stretch: 1.0;
@ -117,6 +121,17 @@ export component MainWindow inherits Window {
text_summary_text = "Searching...";
root.scan_starting(item);
}
show_select_popup(x_offset, y_offset) => {
select_popup_window.x_offset = x_offset;
select_popup_window.y_offset = y_offset;
select_popup_window.show_popup();
}
request_folder_to_move => {
folders_move_choose_requested();
}
show_remove_popup => {
delete_popup_window.show_popup();
}
}
text_summary := LineEdit {
@ -142,11 +157,37 @@ export component MainWindow inherits Window {
width: root.width;
}
// select_popup_window := PopupSelect {
// height: root.height;
// width: root.width;
// }
select_popup_window := PopupSelectResults {
property <length> x_offset: 0;
property <length> y_offset: 0;
x: parent.x + x_offset - self.item_width / 2.0;
y: parent.y + y_offset - self.all_items_height - 5px;
height: root.height;
width: root.width;
}
delete_popup_window := PopupDelete {
height: root.height;
width: root.width;
x: parent.x + (root.width - self.popup_width) / 2.0;
y: parent.y + (parent.height - self.popup_height) / 2.0;
}
move_popup_window := PopupMoveFolders {
height: root.height;
width: root.width;
x: parent.x + (root.width - self.popup_width) / 2.0;
y: parent.y + (parent.height - self.popup_height) / 2.0;
}
show_move_folders_dialog(folder_name) => {
move_popup_window.folder_name = folder_name;
move_popup_window.show_popup();
}
scan_ended(scan_text) => {
text_summary_text = scan_text;

View file

@ -0,0 +1,73 @@
import { Button, VerticalBox ,TextEdit, HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, LineEdit} from "std-widgets.slint";
import {SelectableTableView} from "selectable_tree_view.slint";
import {LeftSidePanel} from "left_side_panel.slint";
import {MainList} from "main_lists.slint";
import {CurrentTab, ProgressToSend} from "common.slint";
import { ActionButtons } from "action_buttons.slint";
import { Progress } from "progress.slint";
import {MainListModel, SelectMode, SelectModel} from "common.slint";
import {Settings} from "settings.slint";
import {Callabler} from "callabler.slint";
import { BottomPanel } from "bottom_panel.slint";
import {ColorPalette} from "color_palette.slint";
import {GuiState} from "gui_state.slint";
import { Preview } from "preview.slint";
export component PopupDelete inherits Rectangle {
out property <length> popup_width: 350px;
out property <length> popup_height: 150px;
callback show_popup();
popup_window := PopupWindow {
width: popup_width;
height: popup_height;
close-on-click: true;
Rectangle {
width: parent.width;
height: parent.height;
border-radius: 5px;
background: ColorPalette.popup_background;
VerticalLayout {
Text {
vertical-stretch: 0.0;
text: "Delete items";
vertical-alignment: center;
horizontal-alignment: center;
font-size: 13px;
padding: 10px;
}
Text {
vertical-stretch: 1.0;
text: "Are you sure you want to delete the selected items?";
vertical-alignment: center;
horizontal-alignment: center;
font-size: 13px;
padding: 10px;
}
HorizontalLayout {
Button {
text: "Yes";
clicked => {
popup_window.close();
Callabler.delete_selected_items();
}
}
Rectangle {
}
Button {
text: "No";
clicked => {
popup_window.close();
}
}
}
}
}
}
show_popup() => {
popup_window.show();
}
}

View file

@ -0,0 +1,94 @@
import { Button, VerticalBox ,TextEdit, HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, LineEdit} from "std-widgets.slint";
import {SelectableTableView} from "selectable_tree_view.slint";
import {LeftSidePanel} from "left_side_panel.slint";
import {MainList} from "main_lists.slint";
import {CurrentTab, ProgressToSend} from "common.slint";
import { ActionButtons } from "action_buttons.slint";
import { Progress } from "progress.slint";
import {MainListModel, SelectMode, SelectModel} from "common.slint";
import {Settings} from "settings.slint";
import {Callabler} from "callabler.slint";
import { BottomPanel } from "bottom_panel.slint";
import {ColorPalette} from "color_palette.slint";
import {GuiState} from "gui_state.slint";
import { Preview } from "preview.slint";
export component PopupMoveFolders inherits Rectangle {
out property <length> popup_width: 500px;
out property <length> popup_height: 150px;
in-out property <string> folder_name: "";
callback show_popup();
popup_window := PopupWindow {
width: popup_width;
height: popup_height;
close-on-click: false;
Rectangle {
width: parent.width;
height: parent.height;
border-radius: 5px;
background: ColorPalette.popup_background;
VerticalLayout {
Text {
vertical-stretch: 0.0;
min-height: 30px;
text: "Moving files";
vertical-alignment: top;
horizontal-alignment: center;
font-size: 13px;
}
Text {
vertical-stretch: 1.0;
text: "Moving entries to folder\n" + folder_name + "\nAre you want to continue?";
vertical-alignment: center;
horizontal-alignment: center;
font-size: 13px;
padding: 10px;
}
VerticalLayout {
HorizontalLayout {
alignment: center;
copy_checkbox := CheckBox {
text: "Copy files instead of moving";
}
}
HorizontalLayout {
alignment: center;
preserve_folder_checkbox := CheckBox {
text: "Preserve folder structure";
}
}
}
HorizontalLayout {
Button {
text: "Yes";
clicked => {
popup_window.close();
Callabler.move_items(preserve_folder_checkbox.checked, copy_checkbox.checked, folder_name);
}
}
Rectangle {
}
Button {
text: "No";
clicked => {
popup_window.close();
}
}
}
}
}
}
init => {
show_popup();
}
show_popup() => {
popup_window.show();
}
}

View file

@ -1,74 +0,0 @@
import { Button, VerticalBox ,TextEdit, HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, LineEdit} from "std-widgets.slint";
import {SelectableTableView} from "selectable_tree_view.slint";
import {LeftSidePanel} from "left_side_panel.slint";
import {MainList} from "main_lists.slint";
import {CurrentTab, ProgressToSend} from "common.slint";
import { ActionButtons } from "action_buttons.slint";
import { Progress } from "progress.slint";
import {MainListModel} from "common.slint";
import {Settings} from "settings.slint";
import {Callabler} from "callabler.slint";
import { BottomPanel } from "bottom_panel.slint";
import {ColorPalette} from "color_palette.slint";
import {GuiState} from "gui_state.slint";
import { Preview } from "preview.slint";
export component PopupSelect inherits Rectangle {
callback show_popup();
popup_window := PopupWindow {
width: root.width;
height: root.height;
property <bool> included_directories;
private property <string> text_data;
close-on-click: false;
HorizontalLayout {
alignment: LayoutAlignment.center;
VerticalLayout {
alignment: LayoutAlignment.center;
Rectangle {
clip: true;
width: root.width - 20px;
height: root.height - 20px;
border-radius: 20px;
background: ColorPalette.popup_background;
VerticalLayout {
Text {
text: "Please add directories one per line";
horizontal-alignment: TextHorizontalAlignment.center;
}
TextEdit {
vertical-stretch: 1.0;
text <=> text-data;
}
HorizontalLayout {
min-height: 20px;
Button {
enabled: text-data != "";
text: "OK";
clicked => {
Callabler.added_manual_directories(GuiState.choosing_include_directories, text_data);
popup_window.close();
}
}
Button {
text: "Cancel";
clicked => {
popup_window.close();
}
}
}
}
}
}
}
}
show_popup() => {
popup_window.show();
}
}

View file

@ -0,0 +1,51 @@
import { Button, VerticalBox ,TextEdit, HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, LineEdit} from "std-widgets.slint";
import {SelectableTableView} from "selectable_tree_view.slint";
import {LeftSidePanel} from "left_side_panel.slint";
import {MainList} from "main_lists.slint";
import {CurrentTab, ProgressToSend} from "common.slint";
import { ActionButtons } from "action_buttons.slint";
import { Progress } from "progress.slint";
import {MainListModel, SelectMode, SelectModel} from "common.slint";
import {Settings} from "settings.slint";
import {Callabler} from "callabler.slint";
import { BottomPanel } from "bottom_panel.slint";
import {ColorPalette} from "color_palette.slint";
import {GuiState} from "gui_state.slint";
import { Preview } from "preview.slint";
export component PopupSelectResults inherits Rectangle {
callback show_popup();
property <[SelectModel]> model: GuiState.select_results_list;
property <length> item_height: 30px;
out property <length> item_width: 200px;
out property <length> all_items_height: item_height * model.length;
popup_window := PopupWindow {
width: item_width;
height: all_items_height;
close-on-click: true;
Rectangle {
width: parent.width;
height: parent.height;
border-radius: 5px;
background: ColorPalette.popup_background;
VerticalLayout {
for i in model: Button {
text: i.name;
height: item_height;
width: item_width;
clicked => {
Callabler.select_items(i.data);
popup_window.close();
}
}
}
}
}
show_popup() => {
popup_window.show();
}
}

Some files were not shown because too many files have changed in this diff Show more