1
0
Fork 0
mirror of synced 2024-04-28 01:22:53 +12:00

Alternative frontend with Slint (#1102)

This commit is contained in:
Rafał Mikrut 2023-12-03 12:06:42 +01:00 committed by GitHub
parent 8df5e991a6
commit c6b1eaeeb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 8380 additions and 806 deletions

View file

@ -12,23 +12,20 @@ jobs:
linux-cli:
strategy:
matrix:
toolchain: [ stable, 1.70.0 ]
toolchain: [ stable, 1.72.1 ]
type: [ release ]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Install basic libraries
run: sudo apt-get update; sudo apt install libheif-dev ffmpeg -y
run: sudo apt update || true; sudo apt install libheif-dev ffmpeg -y
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Build Release
run: cargo build --release --bin czkawka_cli
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-C debuginfo=0"
if: ${{ (matrix.type == 'release') }}
- name: Store Linux CLI

54
.github/workflows/linux_cli_eyra.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: 🐧 Linux CLI Eyra
on:
push:
pull_request:
schedule:
- cron: '0 0 * * 2'
env:
CARGO_TERM_COLOR: always
jobs:
linux-cli:
strategy:
matrix:
toolchain: [ nightly ]
type: [ release ]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- 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-11-29
- name: Add eyra
run: |
cd czkawka_cli
cargo add eyra --rename=std
echo 'fn main() { println!("cargo:rustc-link-arg=-nostartfiles"); }' > build.rs
cd ..
- name: Build Release
run: cargo build --release --bin czkawka_cli
if: ${{ (matrix.type == 'release') }}
- name: Store Linux CLI
uses: actions/upload-artifact@v3
with:
name: czkawka_cli-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/czkawka_cli
if: ${{ matrix.type == 'release' }}
- name: Linux Regression Test
run: |
wget https://github.com/qarmin/czkawka/releases/download/6.0.0/TestFiles.zip
cd ci_tester
cargo build --release
cd ..
ci_tester/target/release/ci_tester target/release/czkawka_cli
if: ${{ matrix.type == 'release' }}

View file

@ -9,48 +9,92 @@ env:
CARGO_TERM_COLOR: always
jobs:
linux-gui:
linux-krokiet-gui:
strategy:
matrix:
toolchain: [ stable, 1.70.0 ]
toolchain: [ stable, 1.72.1 ]
type: [ release ]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Build Release Krokiet
run: cargo build --release --bin krokiet
if: ${{ (matrix.type == 'release') }}
- name: Store Linux GUI Krokiet
uses: actions/upload-artifact@v3
with:
name: krokiet-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/krokiet
if: ${{ matrix.type == 'release' }}
linux-krokiet-gui-heif:
strategy:
matrix:
toolchain: [ stable, 1.72.1 ]
type: [ release ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Install basic libraries
run: sudo apt-get update; sudo apt install libgtk-4-dev libheif-dev -y
run: sudo apt update || true; sudo apt install libheif-dev libraw-dev -y
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Build Release Heif
run: cargo build --release --features heif
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-C debuginfo=0"
if: ${{ (matrix.type == 'release') && (matrix.toolchain == '1.70.0') }}
- name: Build Release Krokiet heif
run: cargo build --release --bin krokiet --features "heif,libraw"
if: ${{ (matrix.type == 'release') }}
- name: Store Linux GUI Heif
- name: Store Linux GUI Krokiet heif libraw
uses: actions/upload-artifact@v3
with:
name: czkawka_gui-${{ runner.os }}-${{ matrix.toolchain }}-heif
path: target/release/czkawka_gui
name: krokiet-${{ runner.os }}-${{ matrix.toolchain }}-heif-libraw
path: target/release/krokiet
if: ${{ matrix.type == 'release' }}
- name: Build Release
run: cargo build --release
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-C debuginfo=0"
linux-gui:
strategy:
matrix:
toolchain: [ stable, 1.72.1 ]
type: [ release ]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Install basic libraries
run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev libraw-dev -y
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- 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
with:
name: czkawka_gui-${{ runner.os }}-${{ matrix.toolchain }}-heif-libraw
path: target/release/czkawka_gui
if: ${{ (matrix.type == 'release') && (matrix.toolchain == 'stable') }}
- name: Build Release
run: cargo build --release --bin czkawka_gui
if: ${{ (matrix.type == 'release') }}
# Only store stable toolchain
- name: Store Linux GUI
uses: actions/upload-artifact@v3
with:
name: czkawka_gui-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/czkawka_gui
if: ${{ matrix.type == 'release' }}
if: ${{ (matrix.type == 'release') && (matrix.toolchain == 'stable') }}
linux-appimage-gui:
strategy:
@ -62,16 +106,13 @@ jobs:
- uses: actions/checkout@v3
- name: Install Dependencies
run: sudo apt-get update; sudo apt install libgtk-4-dev libheif-dev librsvg2-dev wget fuse libfuse2 -y
run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev librsvg2-dev wget fuse libfuse2 -y
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Build Release
run: cargo build --release
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-C debuginfo=0"
run: cargo build --release --bin czkawka_gui
- name: Download appimage dependencies
run: |
@ -118,13 +159,10 @@ jobs:
- uses: actions/checkout@v3
- name: Install Dependencies
run: sudo apt-get update; sudo apt install libgtk-4-dev libheif-dev librsvg2-dev wget fuse libfuse2 -y xvfb
run: sudo apt update || true; sudo apt install libgtk-4-dev libheif-dev librsvg2-dev wget fuse libfuse2 -y xvfb
- name: Setup rust version
run: rustup default ${{ matrix.toolchain }}
- name: Test
run: xvfb-run cargo test
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-C debuginfo=0"

View file

@ -32,8 +32,6 @@ jobs:
- name: Build Release
run: cargo build --release
env:
CARGO_INCREMENTAL: 0
if: ${{ matrix.type == 'release'}}
- name: Store MacOS CLI
@ -50,10 +48,15 @@ jobs:
path: target/release/czkawka_gui
if: ${{ matrix.type == 'release' }}
- name: Store MacOS Krokiet
uses: actions/upload-artifact@v3
with:
name: krokiet-${{ runner.os }}-${{ matrix.toolchain }}
path: target/release/krokiet
if: ${{ matrix.type == 'release' }}
- name: Build Release Heif
run: cargo build --release --features heif
env:
CARGO_INCREMENTAL: 0
if: ${{ matrix.type == 'release'}}
- name: Store MacOS CLI Heif
@ -68,4 +71,11 @@ jobs:
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
with:
name: krokiet-${{ runner.os }}-${{ matrix.toolchain }}-heif
path: target/release/krokiet
if: ${{ matrix.type == 'release' }}

View file

@ -15,16 +15,13 @@ jobs:
- uses: actions/checkout@v3
- name: Install Gtk 4
run: sudo apt-get update; sudo apt install -y libgtk-4-dev libheif-dev -y
run: sudo apt update || true; sudo apt install -y libgtk-4-dev libraw-dev libheif-dev -y
- name: Check the format
run: cargo fmt --all -- --check
# type complexity must be ignored because we use huge templates for queries
- name: Run clippy
run: >
cargo clippy
--all-targets
--all-features
--
-D warnings
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run clippy
run: cargo clippy -- -D warnings

View file

@ -9,47 +9,121 @@ env:
CARGO_TERM_COLOR: always
jobs:
krokiet-compiled-on-linux:
strategy:
fail-fast: false
matrix:
use_heif: [ normal ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies(mostly sd)
run: |
sudo apt update || true;sudo apt install -y mingw-w64 mingw-w64-x86-64-dev
wget https://github.com/chmln/sd/releases/download/v1.0.0/sd-v1.0.0-x86_64-unknown-linux-gnu.tar.gz -O a.tar.gz
tar -xzf a.tar.gz
cp sd-v1.0.0-x86_64-unknown-linux-gnu/sd .
chmod +x ./sd
- name: Setup rust version
run: |
rustup target add x86_64-pc-windows-gnu
- name: Compile Krokiet
run: cargo build --release --target x86_64-pc-windows-gnu --bin krokiet
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: krokiet-windows-c-on-linux-${{ github.sha }}-${{ matrix.use_heif }}
path: |
target/x86_64-pc-windows-gnu/release/krokiet.exe
if-no-files-found: error
- name: Show console window on windows
run: |
./sd -s '#![windows_subsystem = "windows"]' '//#![windows_subsystem = "windows"]' krokiet/src/main.rs
cat krokiet/src/main.rs
- name: Compile Krokiet Console
run: cargo build --release --target x86_64-pc-windows-gnu --bin krokiet
- name: Upload artifacts Console
uses: actions/upload-artifact@v3
with:
name: krokiet-windows-c-on-linux-${{ github.sha }}-${{ matrix.use_heif }}-console
path: |
target/x86_64-pc-windows-gnu/release/krokiet.exe
if-no-files-found: error
krokiet-compiled-on-windows:
strategy:
fail-fast: false
matrix:
use_heif: [ normal ]
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies(mostly sd)
run: |
Invoke-WebRequest -Uri https://github.com/chmln/sd/releases/download/v1.0.0/sd-v1.0.0-x86_64-pc-windows-gnu.zip -OutFile a.zip
Expand-Archive ./a.zip
cp a/sd-v1.0.0-x86_64-pc-windows-gnu/sd.exe .
- name: Setup rust version
run: rustup default stable-x86_64-pc-windows-gnu
- name: Compile Krokiet
run: cargo build --release --bin krokiet
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: krokiet-windows-c-on-windows-${{ github.sha }}-${{ matrix.use_heif }}
path: |
target/release/krokiet.exe
if-no-files-found: error
- name: Show console window on windows
run: |
./sd.exe -s '#![windows_subsystem = "windows"]' '//#![windows_subsystem = "windows"]' krokiet/src/main.rs
cat krokiet/src/main.rs
- name: Compile Krokiet Console
run: cargo build --release --bin krokiet
- name: Upload artifacts Console
uses: actions/upload-artifact@v3
with:
name: krokiet-windows-c-on-windows-${{ github.sha }}-${{ matrix.use_heif }}-console
path: |
target/release/krokiet.exe
if-no-files-found: error
container:
strategy:
fail-fast: false
matrix:
use_heif: [ non_heif ] #, heif ] - heif problems with mingw
runs-on: ubuntu-22.04
use_heif: [ non_heif ]
runs-on: ubuntu-latest
container:
image: ghcr.io/piegamesde/gtk4-cross:gtk-4.6
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: Install additional dependencies
# gio is for the build script
run: dnf install wget2 unzip mingw64-bzip2.noarch mingw64-poppler mingw64-poppler-glib mingw32-python3 rust-gio-devel adwaita-icon-theme -y && dnf clean all -y
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
target: x86_64-pc-windows-gnu
- name: Cache ~/.cargo
uses: actions/cache@v1
with:
path: ~/.cargo
key: windows-dotcargo
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: windows-build-target
- name: Cross compile for Windows Heif
run: |
#!/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 --features heif
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/
if: ${{ matrix.use_heif == 'heif' }}
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/
@ -57,7 +131,6 @@ jobs:
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/
if: ${{ matrix.use_heif == 'non_heif' }}
- name: Package
run: |
@ -95,29 +168,27 @@ jobs:
container:
image: ghcr.io/piegamesde/gtk4-cross:gtk-4.6
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: Install dependencies(mostly sd)
run: |
dnf install wget -y
wget https://github.com/chmln/sd/releases/download/v1.0.0/sd-v1.0.0-x86_64-unknown-linux-gnu.tar.gz -O a.tar.gz
tar -xzf a.tar.gz
chmod +x sd-v1.0.0-x86_64-unknown-linux-gnu/sd
sudo cp sd-v1.0.0-x86_64-unknown-linux-gnu/sd /usr/bin/sd
- name: Install additional dependencies
# gio is for the build script
run: dnf install wget2 unzip mingw64-bzip2.noarch mingw64-poppler mingw64-poppler-glib mingw32-python3 rust-gio-devel adwaita-icon-theme -y && dnf clean all -y
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
target: x86_64-pc-windows-gnu
- name: Cache ~/.cargo
uses: actions/cache@v1
with:
path: ~/.cargo
key: windows-dotcargo
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: windows-build-target
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: Show console window on windows
run: sed -i 's|\#\!\[windows_subsystem|//#![windows_subsystem|' czkawka_gui/src/main.rs
run: sd -s '#![windows_subsystem = "windows"]' '//#![windows_subsystem = "windows"]' krokiet/src/main.rs
- 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/

4
.gitignore vendored
View file

@ -15,6 +15,6 @@ flatpak/
/report
ci_tester/target
ci_tester/Cargo.lock
czkawka_slint_gui/Cargo.lock
czkawka_slint_gui/target
krokiet/Cargo.lock
krokiet/target
*.json

3992
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,10 +3,10 @@ members = [
"czkawka_core",
"czkawka_cli",
"czkawka_gui",
"krokiet"
]
exclude = [
"ci_tester",
"czkawka_slint_gui"
]
resolver = "2"

View file

@ -1,3 +1,22 @@
## Version 7.0.0 - ?
### GTK GUI
- Added drag&drop support for included/excluded folders - [#1106](https://github.com/qarmin/czkawka/pull/1106)
- Added information where are saved scan results - [#1102](https://github.com/qarmin/czkawka/pull/1102)
### CLI
- Providing full static rust binary with [Eyra](https://github.com/sunfishcode/eyra) - [#1102](https://github.com/qarmin/czkawka/pull/1102)
### Krokiet GUI
- Initial release of new gui - [#1102](https://github.com/qarmin/czkawka/pull/1102)
### Core
- Using normal crossbeam channels instead of asyncio tokio channel - [#1102](https://github.com/qarmin/czkawka/pull/1102)
- Fixed tool type when using progress of empty directories - [#1102](https://github.com/qarmin/czkawka/pull/1102)
- Fixed missing json support in saving size and name - [#1102](https://github.com/qarmin/czkawka/pull/1102)
- Fix cross-compiled debug windows build - [#1102](https://github.com/qarmin/czkawka/pull/1102)
- Added bigger stack size by default(fixes stack overflow in some musl apps) - [#1102](https://github.com/qarmin/czkawka/pull/1102)
- Added optional libraw dependency(better single-core performance and support more raw files) - [#1102](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)
- Remove up to 340ms of delay when waiting for results - [#1070](https://github.com/qarmin/czkawka/pull/1070)

101
README.md
View file

@ -27,24 +27,12 @@
![Czkawka](https://user-images.githubusercontent.com/41945903/145280350-506f7e94-4db0-4de7-a68d-6e7c26bbd2bf.gif)
## Supported OS
Linux - Ubuntu 22.04+, Fedora 36+, Alpine Linux 3.16+, Debian 12+ and a lot of more
Windows - 7, 8.1, 10, 11
MacOS - 10.15+
If you are looking for older version that use GTK 3 and have support for more OS(like e.g. Ubuntu 20.04), look at [4.1.0](https://github.com/qarmin/czkawka/releases/tag/4.1.0) or older versions.
## How do I use it?
You can find the instructions on how to use Czkawka [**here**](instructions/Instruction.md).
Some helpful tricks you can find [**here**](instructions/Instruction.md#tips-tricks-and-known-bugs)
## Installation
Installation instructions with download links you can find [**here**](instructions/Installation.md).
## Compilation
If you want to try and develop Czkawka or just use the latest available feature, you may want to look at the [**compilation instructions**](instructions/Compilation.md).
## Usage, installation, compilation, requirements, license
Each tool uses different technologies, so you can find instructions for each of them in the appropriate file:
- [Czkawka GUI (GTK frontend)](czkawka_gui/README.md)</br>
- [Czkawka CLI](czkawka_cli/README.md)</br>
- [Czkawka Core](czkawka_core/README.md)</br>
- [Krokiet GUI (Slint frontend)](krokiet/README.md)</br>
## Benchmarks
@ -99,31 +87,34 @@ Similar images which check 349 image files that occupied 1.7 GB
Bleachbit is a master at finding and removing temporary files, while Czkawka only finds the most basic ones. So these two apps shouldn't be compared directly or be considered as an alternative to one another.
| | Czkawka | FSlint | DupeGuru | Bleachbit |
|:------------------------:|:-----------:|:----------:|:-----------------:|:-----------:|
| Language | Rust | Python | Python/Obj-C | Python |
| OS | Lin,Mac,Win | Lin | Lin,Mac,Win | Lin,Mac,Win |
| Framework | GTK 4 | PyGTK2 | Qt 5 (PyQt)/Cocoa | PyGTK3 |
| Duplicate finder | ✔ | ✔ | ✔ | |
| Empty files | ✔ | ✔ | | |
| Empty folders | ✔ | ✔ | | |
| Temporary files | ✔ | ✔ | | ✔ |
| Big files | ✔ | | | |
| Similar images | ✔ | | ✔ | |
| Similar videos | ✔ | | | |
| Music duplicates(tags) | ✔ | | ✔ | |
| Invalid symlinks | ✔ | ✔ | | |
| Broken files | ✔ | | | |
| Names conflict | ✔ | ✔ | | |
| Invalid names/extensions | ✔ | ✔ | | |
| Installed packages | | ✔ | | |
| Bad ID | | ✔ | | |
| Non stripped binaries | | ✔ | | |
| Redundant whitespace | | ✔ | | |
| Overwriting files | | ✔ | | ✔ |
| Multiple languages | ✔ | ✔ | ✔ | ✔ |
| Cache support | ✔ | | ✔ | |
| In active development | Yes | No | Yes | Yes |
In this comparison remember, that even if app have same features they may work different(e.g. one app may have more options to choose than other).
| | Czkawka | Krokiet | FSlint | DupeGuru | Bleachbit |
|:------------------------:|:-----------:|:-----------:|:------:|:------------------:|:-----------:|
| Language | Rust | Rust | Python | Python/Obj-C | Python |
| Framework base language | C | Rust | C | C/C++/Obj-C/Swift | C |
| Framework | GTK 4 | Slint | PyGTK2 | Qt 5 (PyQt)/Cocoa | PyGTK3 |
| OS | Lin,Mac,Win | Lin,Mac,Win | Lin | Lin,Mac,Win | Lin,Mac,Win |
| Duplicate finder | ✔ | ✔ | ✔ | ✔ | |
| Empty files | ✔ | ✔ | ✔ | | |
| Empty folders | ✔ | ✔ | ✔ | | |
| Temporary files | ✔ | ✔ | ✔ | | ✔ |
| Big files | ✔ | ✔ | | | |
| Similar images | ✔ | ✔ | | ✔ | |
| Similar videos | ✔ | ✔ | | | |
| Music duplicates(tags) | ✔ | ✔ | | ✔ | |
| Invalid symlinks | ✔ | ✔ | ✔ | | |
| Broken files | ✔ | ✔ | | | |
| Names conflict | ✔ | ✔ | ✔ | | |
| Invalid names/extensions | ✔ | ✔ | ✔ | | |
| Installed packages | | | ✔ | | |
| Bad ID | | | ✔ | | |
| Non stripped binaries | | | ✔ | | |
| Redundant whitespace | | | ✔ | | |
| Overwriting files | | | ✔ | | ✔ |
| Multiple languages | ✔ | | ✔ | ✔ | ✔ |
| Cache support | ✔ | ✔ | | ✔ | |
| In active development | Yes | | No | Yes | Yes |
## Other apps
There are many similar applications to Czkawka on the Internet, which do some things better and some things worse:
@ -156,30 +147,6 @@ You can also help by doing other things:
- Creating videos - [First Video](https://www.youtube.com/watch?v=CWlRiTD4vDc) or [Spanish Tutorial](https://www.youtube.com/watch?v=V9x-pHJRmKY)
- Recommending it to others
## Name
Czkawka is a Polish word which means _hiccup_.
I chose this name because I wanted to hear people speaking other languages pronounce it, so feel free to spell it the way you want.
This name is not as bad as it seems, because I was also thinking about using words like _żółć_, _gżegżółka_ or _żołądź_,
but I gave up on these ideas because they contained Polish characters, which would cause difficulty in searching for the project.
At the beginning of the program creation, if the response concerning the name was unanimously negative, I prepared myself
for a possible change of the name of the program, and the opinions were extremely mixed.
## License
Code is distributed under MIT license.
Icon was created by [jannuary](https://github.com/jannuary) and licensed CC-BY-4.0.
Windows dark theme is used from project [WhiteSur](https://github.com/slypy/whitesur-gtk4-theme) with MIT license.
Some icons were taken from [ReShot](https://www.reshot.com) site and are licensed under Reshot Free License.
The program is completely free to use.
"Gratis to uczciwa cena" - "Free is a fair price"
## Thanks
Big thanks to Pádraig Brady, creator of fantastic FSlint, because without his work I wouldn't create this tool.

View file

@ -3,7 +3,7 @@ name = "czkawka_cli"
version = "6.1.0"
authors = ["Rafał Mikrut <mikrutrafal@protonmail.com>"]
edition = "2021"
rust-version = "1.70.0"
rust-version = "1.72.1"
description = "CLI frontend of Czkawka"
license = "MIT"
homepage = "https://github.com/qarmin/czkawka"
@ -23,3 +23,4 @@ czkawka_core = { path = "../czkawka_core", version = "6.1.0", features = [] }
[features]
default = []
heif = ["czkawka_core/heif"]
libraw = ["czkawka_core/libraw"]

21
czkawka_cli/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2023 Rafał Mikrut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

44
czkawka_cli/README.md Normal file
View file

@ -0,0 +1,44 @@
# Czkawka CLI
CLI frontend, allows to use Czkawka from terminal.
## Requirements
Precompiled binaries should work without any additional dependencies with Linux(Ubuntu 20.04+), Windows(10+) and macOS(10.15+).
If you decide to compile the app, you probably will be able to run it on even older versions of OS, like Ubuntu 16.04 or Windows 7.
On linux it is even possible with eyra to avoid entirely libc and using fully static rust binary.
If you want to use similar videos tool, you need to install ffmpeg(runtime dependency) or use heif/libraw(build/runtime dependency) you need to install required packages.
- mac - `brew install ffmpeg libraw libheif` - https://formulae.brew.sh/formula/ffmpeg
- linux - `sudo apt install ffmpeg libraw-dev libheif-dev`
- windows - `choco install ffmpeg` - or if not working, download from https://ffmpeg.org/download.html#build-windows and unpack to location with `czkawka_cli.exe`, heif and libraw are not supported on windows
## Compilation
For compilation, you need to have installed Rust via rustup - https://rustup.rs/ and compile it e.g. via
```shell
cargo run --release --bin czkawka_cli
```
you can enable additional features via
```shell
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
cd czkawka_cli
cargo add eyra --rename=std
echo 'fn main() { println!("cargo:rustc-link-arg=-nostartfiles"); }' > build.rs
cd ..
cargo build --release --bin czkawka_cli
```
## Limitations
Not all available features in core are available in CLI.
List of not available features:
- Ability to use/choose referenced directories
- See progress of scanning
## LICENSE
MIT

View file

@ -3,7 +3,7 @@ name = "czkawka_core"
version = "6.1.0"
authors = ["Rafał Mikrut <mikrutrafal@protonmail.com>"]
edition = "2021"
rust-version = "1.70.0"
rust-version = "1.72.1"
description = "Core of Czkawka app"
license = "MIT"
homepage = "https://github.com/qarmin/czkawka"
@ -28,9 +28,6 @@ hamming = "0.1"
bitflags = "2.4"
lofty = "0.16"
# Futures - needed by async progress sender
futures = "0.3.28"
# Needed by broken files
zip = { version = "0.6", features = ["aes-crypto", "bzip2", "deflate", "time"], default-features = false }
audio_checker = "0.1"
@ -59,12 +56,13 @@ serde_json = "1.0"
# Language
i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.7"
rust-embed = "8.0"
rust-embed = { version = "8.0", features = ["debug-embed"] }
once_cell = "1.18"
# Raw image files
rawloader = "0.37"
imagepipe = "0.5"
libraw-rs = { version = "0.0.4", optional = true }
# Checking for invalid extensions
mime_guess = "2.0"
@ -72,11 +70,14 @@ infer = "0.15"
# Heif/Heic
libheif-rs = { version = "=0.18.0", optional = true } # Do not upgrade now, since Ubuntu 22.04 not works with newer version
libheif-sys = { version = "=1.14.2", optional = true } # 1.14.3 brake compilation on Ubuntu 22.04
libheif-sys = { version = "=1.14.2", optional = true } # 1.14.3 brake compilation on Ubuntu 22.04, so pin it to this version
anyhow = { version = "1.0" }
state = "0.6"
os_info = { version = "3", default-features = false }
rustc_version = "0.4"
log = "0.4.20"
handsome_logger = "0.8"
fun_time = { version = "0.3.1", features = ["log"] }
@ -84,3 +85,4 @@ fun_time = { version = "0.3.1", features = ["log"] }
[features]
default = []
heif = ["dep:libheif-rs", "dep:libheif-sys"]
libraw = ["dep:libraw-rs"]

21
czkawka_core/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2023 Rafał Mikrut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
czkawka_core/README.md Normal file
View file

@ -0,0 +1,3 @@
# Czkawka Core
Core of Czkawka GUI/CLI and Krokiet projects.

View file

@ -5,9 +5,8 @@ use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use log::debug;
use mime_guess::get_mime_extensions;
use rayon::prelude::*;
@ -161,7 +160,7 @@ const WORKAROUNDS: &[(&str, &str)] = &[
("exe", "xls"), // Not sure why xls is not recognized
];
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, Debug)]
pub struct BadFileEntry {
pub path: PathBuf,
pub modified_date: u64,
@ -195,7 +194,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<&UnboundedSender<ProgressData>>) {
pub fn find_bad_extensions_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -209,7 +208,7 @@ impl BadExtensions {
}
#[fun_time(message = "check_files", level = "debug")]
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let result = DirTraversalBuilder::new()
.root_dirs(self.common_data.directories.included_directories.clone())
.group_by(|_fe| ())
@ -221,6 +220,7 @@ impl BadExtensions {
.allowed_extensions(self.common_data.allowed_extensions.clone())
.excluded_items(self.common_data.excluded_items.clone())
.recursive_search(self.common_data.recursive_search)
.tool_type(self.common_data.tool_type)
.build()
.run();
@ -239,7 +239,7 @@ impl BadExtensions {
}
#[fun_time(message = "look_for_bad_extensions_files", level = "debug")]
fn look_for_bad_extensions_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn look_for_bad_extensions_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let (progress_thread_handle, progress_thread_run, atomic_counter, check_was_stopped) =
prepare_thread_handler_common(progress_sender, 1, 1, self.files_to_check.len(), CheckingMethod::None, self.get_cd().tool_type);

View file

@ -6,9 +6,8 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use humansize::{format_size, BINARY};
use log::debug;
use rayon::prelude::*;
@ -57,7 +56,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<&UnboundedSender<ProgressData>>) {
pub fn find_big_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
if !self.look_for_big_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -68,7 +67,7 @@ impl BigFile {
}
#[fun_time(message = "look_for_big_files", level = "debug")]
fn look_for_big_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn look_for_big_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let mut folders_to_check: Vec<PathBuf> = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector
let mut old_map: BTreeMap<u64, Vec<FileEntry>> = Default::default();

View file

@ -7,9 +7,8 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::{fs, mem, panic};
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use log::debug;
use pdf::file::FileOptions;
use pdf::object::ParseOptions;
@ -27,7 +26,7 @@ use crate::common_dir_traversal::{common_get_entry_data_metadata, common_read_di
use crate::common_tool::{CommonData, CommonToolData, DeleteMethod};
use crate::common_traits::*;
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct FileEntry {
pub path: PathBuf,
pub modified_date: u64,
@ -93,7 +92,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<&UnboundedSender<ProgressData>>) {
pub fn find_broken_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -108,7 +107,7 @@ impl BrokenFiles {
}
#[fun_time(message = "check_files", level = "debug")]
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let mut folders_to_check: Vec<PathBuf> = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector
// Add root folders for finding
@ -191,9 +190,7 @@ impl BrokenFiles {
) -> Option<FileEntry> {
atomic_counter.fetch_add(1, Ordering::Relaxed);
let Some(file_name_lowercase) = get_lowercase_name(entry_data, warnings) else {
return None;
};
let file_name_lowercase = get_lowercase_name(entry_data, warnings)?;
if !self.common_data.allowed_extensions.matches_filename(&file_name_lowercase) {
return None;
@ -347,7 +344,7 @@ impl BrokenFiles {
}
#[fun_time(message = "look_for_broken_files", level = "debug")]
fn look_for_broken_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn look_for_broken_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache();
let (progress_thread_handle, progress_thread_run, atomic_counter, _check_was_stopped) =

View file

@ -1,24 +1,29 @@
#![allow(unused_imports)]
// I don't wanna fight with unused imports in this file, so simply ignore it to avoid too much complexity
use std::ffi::OsString;
use std::fs::{DirEntry, File, OpenOptions};
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread::{sleep, JoinHandle};
use std::time::{Duration, SystemTime};
use std::time::{Duration, Instant, SystemTime};
use std::{fs, thread};
#[cfg(feature = "heif")]
use anyhow::Result;
use crossbeam_channel::Sender;
use directories_next::ProjectDirs;
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use handsome_logger::{ColorChoice, ConfigBuilder, TerminalMode};
use image::{DynamicImage, ImageBuffer, Rgb};
use imagepipe::{ImageSource, Pipeline};
#[cfg(feature = "heif")]
use libheif_rs::{ColorSpace, HeifContext, RgbChroma};
use log::{info, LevelFilter, Record};
#[cfg(feature = "libraw")]
use libraw::Processor;
use log::{debug, error, info, warn, LevelFilter, Record};
use rawloader::RawLoader;
use symphonia::core::conv::IntoSample;
// #[cfg(feature = "heif")]
// use libheif_rs::LibHeif;
@ -32,6 +37,13 @@ use crate::duplicate::make_hard_link;
use crate::CZKAWKA_VERSION;
static NUMBER_OF_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();
@ -44,7 +56,7 @@ pub fn get_number_of_threads() -> usize {
fn filtering_messages(record: &Record) -> bool {
if let Some(module_path) = record.module_path() {
module_path.starts_with("czkawka")
module_path.starts_with("czkawka") || module_path.starts_with("krokiet")
} else {
true
}
@ -57,12 +69,30 @@ 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 print_version_mode() {
let rust_version = match rustc_version::version_meta() {
Ok(meta) => meta.semver.to_string(),
Err(_) => "<unknown>".to_string(),
};
let debug_release = if cfg!(debug_assertions) { "debug" } else { "release" };
let processors = get_available_threads();
let info = os_info::get();
info!(
"Czkawka version: {}, was compiled with {} mode",
CZKAWKA_VERSION,
if cfg!(debug_assertions) { "debug" } else { "release" }
"App version: {CZKAWKA_VERSION}, {debug_release} mode, rust {rust_version}, os {} {} [{} {}], {processors} cpu/threads",
info.os_type(),
info.version(),
std::env::consts::ARCH,
info.bitness(),
);
if cfg!(debug_assertions) {
warn!("You are running debug version of app which is a lot of slower than release version.");
}
}
pub fn set_default_number_of_threads() {
@ -76,7 +106,18 @@ pub fn get_default_number_of_threads() -> usize {
pub fn set_number_of_threads(thread_number: usize) {
NUMBER_OF_THREADS.set(thread_number);
rayon::ThreadPoolBuilder::new().num_threads(get_number_of_threads()).build_global().unwrap();
let additional_message = if thread_number == 0 {
" (0 - means that all available threads will be used)"
} else {
""
};
debug!("Number of threads set to {thread_number}{additional_message}");
rayon::ThreadPoolBuilder::new()
.num_threads(get_number_of_threads())
.stack_size(DEFAULT_WORKER_THREAD_SIZE)
.build_global()
.unwrap();
}
pub const RAW_IMAGE_EXTENSIONS: &[&str] = &[
@ -111,6 +152,46 @@ pub const SEND_PROGRESS_DATA_TIME_BETWEEN: u32 = 200; //ms
pub struct Common();
pub fn remove_folder_if_contains_only_empty_folders(path: impl AsRef<Path>) -> bool {
let path = path.as_ref();
if !path.is_dir() {
error!("Trying to remove folder which is not a directory");
return false;
}
let mut entries_to_check = Vec::new();
let Ok(initial_entry) = path.read_dir() else {
return false;
};
for entry in initial_entry {
if let Ok(entry) = entry {
entries_to_check.push(entry);
} else {
return false;
}
}
loop {
let Some(entry) = entries_to_check.pop() else {
break;
};
if !entry.path().is_dir() {
return false;
}
let Ok(internal_read_dir) = entry.path().read_dir() else {
return false;
};
for internal_elements in internal_read_dir {
if let Ok(internal_element) = internal_elements {
entries_to_check.push(internal_element);
} else {
return false;
}
}
}
fs::remove_dir_all(path).is_ok()
}
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))> {
if let Some(proj_dirs) = ProjectDirs::from("pl", "Qarmin", "Czkawka") {
let cache_dir = PathBuf::from(proj_dirs.cache_dir());
@ -152,10 +233,7 @@ pub fn open_cache_folder(cache_file_name: &str, save_to_cache: bool, use_json: b
file_handler_default = Some(t);
} else {
if use_json {
file_handler_json = Some(match OpenOptions::new().read(true).open(&cache_file_json) {
Ok(t) => t,
Err(_) => return None,
});
file_handler_json = Some(OpenOptions::new().read(true).open(&cache_file_json).ok()?);
} else {
// messages.push(format!("Cannot find or open cache file {}", cache_file.display())); // No error or warning
return None;
@ -183,45 +261,64 @@ pub fn get_dynamic_image_from_heic(path: &str) -> Result<DynamicImage> {
.ok_or_else(|| anyhow::anyhow!("Failed to create image buffer"))
}
pub fn get_dynamic_image_from_raw_image(path: impl AsRef<Path> + std::fmt::Debug) -> Option<DynamicImage> {
let file_handler = match OpenOptions::new().read(true).open(&path) {
Ok(t) => t,
Err(_e) => {
return None;
}
};
#[cfg(feature = "libraw")]
pub fn get_dynamic_image_from_raw_image(path: impl AsRef<Path>) -> Option<DynamicImage> {
let buf = fs::read(path.as_ref()).ok()?;
let mut reader = BufReader::new(file_handler);
let raw = match rawloader::decode(&mut reader) {
Ok(raw) => raw,
Err(_e) => {
return None;
}
};
let processor = Processor::new();
let start_timer = Instant::now();
let processed = processor.process_8bit(&buf).expect("processing successful");
println!("Processing took {:?}", start_timer.elapsed());
let width = processed.width();
let height = processed.height();
dbg!(width, height);
let data = processed.to_vec();
let buffer = ImageBuffer::from_raw(width, height, data)?;
// Utwórz DynamicImage z ImageBuffer
Some(DynamicImage::ImageRgb8(buffer))
}
#[cfg(not(feature = "libraw"))]
pub fn get_dynamic_image_from_raw_image(path: impl AsRef<Path> + std::fmt::Debug) -> Option<DynamicImage> {
let mut start_timer = Instant::now();
let mut times = Vec::new();
let loader = RawLoader::new();
let raw = loader.decode_file(path.as_ref()).ok()?;
times.push(("After decoding", start_timer.elapsed()));
start_timer = Instant::now();
let source = ImageSource::Raw(raw);
let mut pipeline = match Pipeline::new_from_source(source) {
Ok(pipeline) => pipeline,
Err(_e) => {
return None;
}
};
times.push(("After creating source", start_timer.elapsed()));
start_timer = Instant::now();
let mut pipeline = Pipeline::new_from_source(source).ok()?;
times.push(("After creating pipeline", start_timer.elapsed()));
start_timer = Instant::now();
pipeline.run(None);
let image = match pipeline.output_8bit(None) {
Ok(image) => image,
Err(_e) => {
return None;
}
};
let image = pipeline.output_8bit(None).ok()?;
let Some(image) = ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(image.width as u32, image.height as u32, image.data) else {
return None;
};
times.push(("After creating image", start_timer.elapsed()));
start_timer = Instant::now();
let image = ImageBuffer::<Rgb<u8>, Vec<u8>>::from_raw(image.width as u32, image.height as u32, image.data)?;
times.push(("After creating image buffer", start_timer.elapsed()));
start_timer = Instant::now();
// println!("Properly hashed {:?}", path);
Some(DynamicImage::ImageRgb8(image))
let res = Some(DynamicImage::ImageRgb8(image));
times.push(("After creating dynamic image", start_timer.elapsed()));
let str_timer = times.into_iter().map(|(name, time)| format!("{name}: {time:?}")).collect::<Vec<_>>().join(", ");
debug!("Loading raw image --- {str_timer}");
res
}
pub fn split_path(path: &Path) -> (String, String) {
@ -460,13 +557,14 @@ where
}
pub fn prepare_thread_handler_common(
progress_sender: Option<&UnboundedSender<ProgressData>>,
progress_sender: Option<&Sender<ProgressData>>,
current_stage: u8,
max_stage: u8,
max_value: usize,
checking_method: CheckingMethod,
tool_type: ToolType,
) -> (JoinHandle<()>, Arc<AtomicBool>, Arc<AtomicUsize>, AtomicBool) {
assert_ne!(tool_type, ToolType::None, "ToolType::None should not exist");
let progress_thread_run = Arc::new(AtomicBool::new(true));
let atomic_counter = Arc::new(AtomicUsize::new(0));
let check_was_stopped = AtomicBool::new(false);
@ -480,7 +578,7 @@ pub fn prepare_thread_handler_common(
loop {
if time_since_last_send.elapsed().unwrap().as_millis() > SEND_PROGRESS_DATA_TIME_BETWEEN as u128 {
progress_send
.unbounded_send(ProgressData {
.send(ProgressData {
checking_method,
current_stage,
max_stage,
@ -521,9 +619,36 @@ pub fn send_info_and_wait_for_ending_all_threads(progress_thread_run: &Arc<Atomi
#[cfg(test)]
mod test {
use std::path::PathBuf;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use tempfile::tempdir;
use crate::common::Common;
use crate::common::{remove_folder_if_contains_only_empty_folders, Common};
#[test]
fn test_remove_folder_if_contains_only_empty_folders() {
let dir = tempdir().unwrap();
let sub_dir = dir.path().join("sub_dir");
fs::create_dir(&sub_dir).unwrap();
// Test with empty directory
assert!(remove_folder_if_contains_only_empty_folders(&sub_dir));
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!(!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!(Path::new(&sub_dir).exists());
}
#[test]
fn test_regex() {

View file

@ -5,9 +5,8 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use std::time::UNIX_EPOCH;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use log::debug;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
@ -136,7 +135,7 @@ pub struct DirTraversalBuilder<'a, 'b, F> {
group_by: Option<F>,
root_dirs: Vec<PathBuf>,
stop_receiver: Option<&'a Receiver<()>>,
progress_sender: Option<&'b UnboundedSender<ProgressData>>,
progress_sender: Option<&'b Sender<ProgressData>>,
minimal_file_size: Option<u64>,
maximal_file_size: Option<u64>,
checking_method: CheckingMethod,
@ -153,7 +152,7 @@ pub struct DirTraversal<'a, 'b, F> {
group_by: F,
root_dirs: Vec<PathBuf>,
stop_receiver: Option<&'a Receiver<()>>,
progress_sender: Option<&'b UnboundedSender<ProgressData>>,
progress_sender: Option<&'b Sender<ProgressData>>,
recursive_search: bool,
directories: Directories,
excluded_items: ExcludedItems,
@ -188,7 +187,7 @@ impl<'a, 'b> DirTraversalBuilder<'a, 'b, ()> {
directories: None,
allowed_extensions: None,
excluded_items: None,
tool_type: ToolType::BadExtensions,
tool_type: ToolType::None,
}
}
}
@ -204,7 +203,7 @@ impl<'a, 'b, F> DirTraversalBuilder<'a, 'b, F> {
self
}
pub fn progress_sender(mut self, progress_sender: Option<&'b UnboundedSender<ProgressData>>) -> Self {
pub fn progress_sender(mut self, progress_sender: Option<&'b Sender<ProgressData>>) -> Self {
self.progress_sender = progress_sender;
self
}
@ -342,6 +341,8 @@ where
{
#[fun_time(message = "run(collecting files/dirs)", level = "debug")]
pub fn run(self) -> DirTraversalResult<T> {
assert!(self.tool_type != ToolType::None, "Tool type cannot be None");
let mut all_warnings = vec![];
let mut grouped_file_entries: BTreeMap<T, Vec<FileEntry>> = BTreeMap::new();
let mut folder_entries: BTreeMap<PathBuf, FolderEntry> = BTreeMap::new();

View file

@ -2,6 +2,11 @@ use std::path::Path;
use crate::common::Common;
#[cfg(target_family = "unix")]
pub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &["/proc", "/dev", "/sys", "/run", "/snap"];
#[cfg(not(target_family = "unix"))]
pub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &["C:\\Windows"];
#[cfg(target_family = "unix")]
pub const DEFAULT_EXCLUDED_ITEMS: &str = "*/.git/*,*/node_modules/*,*/lost+found/*,*/Trash/*,*/.Trash-*/*,*/snap/*,/home/*/.cache/*";
#[cfg(not(target_family = "unix"))]

View file

@ -35,7 +35,7 @@ pub trait PrintResults {
fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()>;
fn save_results_to_file_as_json_internal<T: Serialize>(&self, file_name: &str, item_to_serialize: &T, pretty_print: bool) -> std::io::Result<()> {
fn save_results_to_file_as_json_internal<T: Serialize + std::fmt::Debug>(&self, file_name: &str, item_to_serialize: &T, pretty_print: bool) -> std::io::Result<()> {
if pretty_print {
self.save_results_to_file_as_json_pretty(file_name, item_to_serialize)
} else {
@ -44,7 +44,7 @@ pub trait PrintResults {
}
#[fun_time(message = "save_results_to_file_as_json_pretty", level = "debug")]
fn save_results_to_file_as_json_pretty<T: Serialize>(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> {
fn save_results_to_file_as_json_pretty<T: Serialize + std::fmt::Debug>(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> {
let file_handler = File::create(file_name)?;
let mut writer = BufWriter::new(file_handler);
serde_json::to_writer_pretty(&mut writer, item_to_serialize)?;
@ -52,7 +52,7 @@ pub trait PrintResults {
}
#[fun_time(message = "save_results_to_file_as_json_compact", level = "debug")]
fn save_results_to_file_as_json_compact<T: Serialize>(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> {
fn save_results_to_file_as_json_compact<T: Serialize + std::fmt::Debug>(&self, file_name: &str, item_to_serialize: &T) -> std::io::Result<()> {
let file_handler = File::create(file_name)?;
let mut writer = BufWriter::new(file_handler);
serde_json::to_writer(&mut writer, item_to_serialize)?;

View file

@ -10,9 +10,8 @@ use std::path::Path;
use std::sync::atomic::Ordering;
use std::{fs, mem};
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use humansize::{format_size, BINARY};
use log::debug;
use rayon::prelude::*;
@ -111,7 +110,7 @@ impl DuplicateFinder {
}
#[fun_time(message = "find_duplicates", level = "info")]
pub fn find_duplicates(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) {
pub fn find_duplicates(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty();
@ -151,7 +150,7 @@ impl DuplicateFinder {
}
#[fun_time(message = "check_files_name", level = "debug")]
fn check_files_name(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files_name(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let group_by_func = if self.case_sensitive_name_comparison {
|fe: &FileEntry| fe.path.file_name().unwrap().to_string_lossy().to_string()
} else {
@ -170,6 +169,7 @@ impl DuplicateFinder {
.recursive_search(self.common_data.recursive_search)
.minimal_file_size(self.common_data.minimal_file_size)
.maximal_file_size(self.common_data.maximal_file_size)
.tool_type(self.common_data.tool_type)
.build()
.run();
@ -226,7 +226,7 @@ impl DuplicateFinder {
}
#[fun_time(message = "check_files_size_name", level = "debug")]
fn check_files_size_name(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files_size_name(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let group_by_func = if self.case_sensitive_name_comparison {
|fe: &FileEntry| (fe.size, fe.path.file_name().unwrap().to_string_lossy().to_string())
} else {
@ -245,6 +245,7 @@ impl DuplicateFinder {
.recursive_search(self.common_data.recursive_search)
.minimal_file_size(self.common_data.minimal_file_size)
.maximal_file_size(self.common_data.maximal_file_size)
.tool_type(self.common_data.tool_type)
.build()
.run();
@ -303,7 +304,7 @@ impl DuplicateFinder {
}
#[fun_time(message = "check_files_size", level = "debug")]
fn check_files_size(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files_size(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let max_stage = match self.check_method {
CheckingMethod::Size => 0,
CheckingMethod::Hash => MAX_STAGE,
@ -322,6 +323,7 @@ impl DuplicateFinder {
.recursive_search(self.common_data.recursive_search)
.minimal_file_size(self.common_data.minimal_file_size)
.maximal_file_size(self.common_data.maximal_file_size)
.tool_type(self.common_data.tool_type)
.build()
.run();
@ -491,7 +493,7 @@ impl DuplicateFinder {
fn prehashing(
&mut self,
stop_receiver: Option<&Receiver<()>>,
progress_sender: Option<&UnboundedSender<ProgressData>>,
progress_sender: Option<&Sender<ProgressData>>,
pre_checked_map: &mut BTreeMap<u64, Vec<FileEntry>>,
) -> Option<()> {
let check_type = self.hash_type;
@ -679,12 +681,7 @@ impl DuplicateFinder {
}
#[fun_time(message = "full_hashing", level = "debug")]
fn full_hashing(
&mut self,
stop_receiver: Option<&Receiver<()>>,
progress_sender: Option<&UnboundedSender<ProgressData>>,
pre_checked_map: BTreeMap<u64, Vec<FileEntry>>,
) -> Option<()> {
fn full_hashing(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>, pre_checked_map: BTreeMap<u64, Vec<FileEntry>>) -> Option<()> {
let (progress_thread_handle, progress_thread_run, _atomic_counter, _check_was_stopped) =
prepare_thread_handler_common(progress_sender, 4, MAX_STAGE, 0, self.check_method, self.common_data.tool_type);
@ -805,7 +802,7 @@ impl DuplicateFinder {
}
#[fun_time(message = "check_files_hash", level = "debug")]
fn check_files_hash(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files_hash(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
assert_eq!(self.check_method, CheckingMethod::Hash);
let mut pre_checked_map: BTreeMap<u64, Vec<FileEntry>> = Default::default();
@ -1163,11 +1160,14 @@ impl PrintResults for DuplicateFinder {
Ok(())
}
// TODO - check if is possible to save also data in header about size and name in SizeName mode - https://github.com/qarmin/czkawka/issues/1137
fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> io::Result<()> {
if self.get_use_reference() {
match self.check_method {
CheckingMethod::Name => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_names_referenced, pretty_print),
CheckingMethod::SizeName => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names_referenced, pretty_print),
CheckingMethod::SizeName => {
self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names_referenced.values().collect::<Vec<_>>(), pretty_print)
}
CheckingMethod::Size => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_referenced, pretty_print),
CheckingMethod::Hash => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_hashes_referenced, pretty_print),
_ => panic!(),
@ -1175,7 +1175,7 @@ impl PrintResults for DuplicateFinder {
} else {
match self.check_method {
CheckingMethod::Name => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_names, pretty_print),
CheckingMethod::SizeName => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names, pretty_print),
CheckingMethod::SizeName => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size_names.values().collect::<Vec<_>>(), pretty_print),
CheckingMethod::Size => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_size, pretty_print),
CheckingMethod::Hash => self.save_results_to_file_as_json_internal(file_name, &self.files_with_identical_hashes, pretty_print),
_ => panic!(),

View file

@ -2,9 +2,8 @@ use std::fs;
use std::io::prelude::*;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use log::debug;
use crate::common_dir_traversal::{DirTraversalBuilder, DirTraversalResult, FileEntry, ProgressData, ToolType};
@ -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<&UnboundedSender<ProgressData>>) {
pub fn find_empty_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -52,7 +51,7 @@ impl EmptyFiles {
}
#[fun_time(message = "check_files", level = "debug")]
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let result = DirTraversalBuilder::new()
.root_dirs(self.common_data.directories.included_directories.clone())
.group_by(|_fe| ())
@ -64,6 +63,7 @@ impl EmptyFiles {
.allowed_extensions(self.common_data.allowed_extensions.clone())
.excluded_items(self.common_data.excluded_items.clone())
.recursive_search(self.common_data.recursive_search)
.tool_type(self.common_data.tool_type)
.build()
.run();

View file

@ -3,9 +3,8 @@ use std::fs;
use std::io::Write;
use std::path::PathBuf;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use log::debug;
use rayon::prelude::*;
@ -42,7 +41,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<&UnboundedSender<ProgressData>>) {
pub fn find_empty_folders(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
if !self.check_for_empty_folders(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -74,7 +73,7 @@ impl EmptyFolder {
}
#[fun_time(message = "check_for_empty_folders", level = "debug")]
fn check_for_empty_folders(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_for_empty_folders(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let result = DirTraversalBuilder::new()
.root_dirs(self.common_data.directories.included_directories.clone())
.group_by(|_fe| ())
@ -84,6 +83,7 @@ impl EmptyFolder {
.excluded_items(self.common_data.excluded_items.clone())
.collect(Collect::EmptyFolders)
.max_stage(0)
.tool_type(self.common_data.tool_type)
.build()
.run();

View file

@ -2,9 +2,8 @@ use std::fs;
use std::io::prelude::*;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use log::debug;
use crate::common_dir_traversal::{Collect, DirTraversalBuilder, DirTraversalResult, ErrorType, FileEntry, ProgressData, ToolType};
@ -31,7 +30,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<&UnboundedSender<ProgressData>>) {
pub fn find_invalid_links(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -42,7 +41,7 @@ impl InvalidSymlinks {
}
#[fun_time(message = "check_files", level = "debug")]
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let result = DirTraversalBuilder::new()
.root_dirs(self.common_data.directories.included_directories.clone())
.group_by(|_fe| ())
@ -53,6 +52,7 @@ impl InvalidSymlinks {
.allowed_extensions(self.common_data.allowed_extensions.clone())
.excluded_items(self.common_data.excluded_items.clone())
.recursive_search(self.common_data.recursive_search)
.tool_type(self.common_data.tool_type)
.build()
.run();

View file

@ -9,9 +9,9 @@ use std::sync::Arc;
use std::{mem, panic};
use anyhow::Context;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use humansize::{format_size, BINARY};
use lofty::{read_from, AudioFile, ItemKey, TaggedFileExt};
use log::debug;
@ -137,7 +137,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<&UnboundedSender<ProgressData>>) {
pub fn find_same_music(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty();
if !self.check_files(stop_receiver, progress_sender) {
@ -176,7 +176,7 @@ impl SameMusic {
}
#[fun_time(message = "check_files", level = "debug")]
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
if !self.common_data.allowed_extensions.using_custom_extensions() {
self.common_data.allowed_extensions.extend_allowed_extensions(AUDIO_FILES_EXTENSIONS);
} else {
@ -203,6 +203,7 @@ impl SameMusic {
.allowed_extensions(self.common_data.allowed_extensions.clone())
.excluded_items(self.common_data.excluded_items.clone())
.recursive_search(self.common_data.recursive_search)
.tool_type(self.common_data.tool_type)
.max_stage(max_stage)
.build()
.run();
@ -277,7 +278,7 @@ impl SameMusic {
}
#[fun_time(message = "calculate_fingerprint", level = "debug")]
fn calculate_fingerprint(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn calculate_fingerprint(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let (progress_thread_handle, progress_thread_run, _atomic_counter, _check_was_stopped) =
prepare_thread_handler_common(progress_sender, 1, MAX_STAGE_CONTENT, 0, self.check_type, self.common_data.tool_type);
@ -341,7 +342,7 @@ impl SameMusic {
}
#[fun_time(message = "read_tags", level = "debug")]
fn read_tags(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn read_tags(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let (progress_thread_handle, progress_thread_run, _atomic_counter, _check_was_stopped) =
prepare_thread_handler_common(progress_sender, 1, MAX_STAGE_TAGS, 0, self.check_type, self.common_data.tool_type);
@ -404,7 +405,7 @@ impl SameMusic {
}
#[fun_time(message = "check_for_duplicate_tags", level = "debug")]
fn check_for_duplicate_tags(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_for_duplicate_tags(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let (progress_thread_handle, progress_thread_run, atomic_counter, _check_was_stopped) =
prepare_thread_handler_common(progress_sender, 4, MAX_STAGE_TAGS, self.music_to_check.len(), self.check_type, self.common_data.tool_type);
@ -503,7 +504,7 @@ impl SameMusic {
true
}
#[fun_time(message = "read_tags_to_files_similar_by_content", level = "debug")]
fn read_tags_to_files_similar_by_content(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn read_tags_to_files_similar_by_content(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let groups_to_check = max(self.duplicated_music_entries.len(), self.duplicated_music_entries_referenced.len());
let (progress_thread_handle, progress_thread_run, atomic_counter, check_was_stopped) =
prepare_thread_handler_common(progress_sender, 5, MAX_STAGE_CONTENT, groups_to_check, self.check_type, self.common_data.tool_type);
@ -629,7 +630,7 @@ impl SameMusic {
}
#[fun_time(message = "check_for_duplicate_fingerprints", level = "debug")]
fn check_for_duplicate_fingerprints(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_for_duplicate_fingerprints(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let (base_files, files_to_compare) = self.split_fingerprints_to_check();
let (progress_thread_handle, progress_thread_run, atomic_counter, _check_was_stopped) =
prepare_thread_handler_common(progress_sender, 2, 3, base_files.len(), self.check_type, self.common_data.tool_type);

View file

@ -7,9 +7,9 @@ use std::time::SystemTime;
use std::{mem, panic};
use bk_tree::BKTree;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use humansize::{format_size, BINARY};
use image::GenericImageView;
use image_hasher::{FilterType, HashAlg, HasherConfig};
@ -94,7 +94,7 @@ pub struct SimilarImages {
// Hashmap with image hashes and Vector with names of files
similarity: u32,
images_to_check: BTreeMap<String, FileEntry>,
hash_size: u8,
pub hash_size: u8, // TODO to remove pub, this is needeed by new gui, because there is no way to check what exactly was seelected
hash_alg: HashAlg,
image_filter: FilterType,
exclude_images_with_same_size: bool,
@ -125,7 +125,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<&UnboundedSender<ProgressData>>) {
pub fn find_similar_images(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
self.common_data.use_reference_folders = !self.common_data.directories.reference_directories.is_empty();
if !self.check_for_similar_images(stop_receiver, progress_sender) {
@ -145,7 +145,7 @@ impl SimilarImages {
}
#[fun_time(message = "check_for_similar_images", level = "debug")]
fn check_for_similar_images(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_for_similar_images(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let mut folders_to_check: Vec<PathBuf> = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector
if !self.common_data.allowed_extensions.using_custom_extensions() {
@ -304,7 +304,7 @@ impl SimilarImages {
// - Join all hashes and save it to file
#[fun_time(message = "hash_images", level = "debug")]
fn hash_images(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn hash_images(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.hash_images_load_cache();
let (progress_thread_handle, progress_thread_run, atomic_counter, check_was_stopped) =
@ -550,7 +550,7 @@ impl SimilarImages {
&mut self,
all_hashed_images: &HashMap<ImHash, Vec<FileEntry>>,
collected_similar_images: &mut HashMap<ImHash, Vec<FileEntry>>,
progress_sender: Option<&UnboundedSender<ProgressData>>,
progress_sender: Option<&Sender<ProgressData>>,
stop_receiver: Option<&Receiver<()>>,
tolerance: u32,
) -> bool {
@ -684,7 +684,7 @@ impl SimilarImages {
}
#[fun_time(message = "find_similar_hashes", level = "debug")]
fn find_similar_hashes(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn find_similar_hashes(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
if self.image_hashes.is_empty() {
return true;
}
@ -1132,6 +1132,7 @@ mod tests {
use std::collections::HashMap;
use std::path::PathBuf;
use crate::common_dir_traversal::ToolType;
use bk_tree::BKTree;
use crate::common_directory::Directories;
@ -1152,6 +1153,10 @@ mod tests {
for _ in 0..100 {
let mut similar_images = SimilarImages {
similarity: 0,
common_data: CommonToolData {
tool_type: ToolType::SimilarImages,
..Default::default()
},
..Default::default()
};
@ -1183,6 +1188,10 @@ mod tests {
for _ in 0..100 {
let mut similar_images = SimilarImages {
similarity: 1,
common_data: CommonToolData {
tool_type: ToolType::SimilarImages,
..Default::default()
},
..Default::default()
};
@ -1203,6 +1212,7 @@ mod tests {
similarity: 2,
common_data: CommonToolData {
use_reference_folders: false,
tool_type: ToolType::SimilarImages,
..Default::default()
},
..Default::default()
@ -1227,6 +1237,10 @@ mod tests {
// 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()
// };
@ -1250,6 +1264,7 @@ mod tests {
let mut similar_images = SimilarImages {
similarity: 0,
common_data: CommonToolData {
tool_type: ToolType::SimilarImages,
directories: Directories {
reference_directories: vec![PathBuf::from("/home/rr/")],
..Default::default()
@ -1276,6 +1291,7 @@ mod tests {
let mut similar_images = SimilarImages {
similarity: 0,
common_data: CommonToolData {
tool_type: ToolType::SimilarImages,
directories: Directories {
reference_directories: vec![PathBuf::from("/home/rr/")],
..Default::default()
@ -1303,6 +1319,7 @@ mod tests {
let mut similar_images = SimilarImages {
similarity: 0,
common_data: CommonToolData {
tool_type: ToolType::SimilarImages,
directories: Directories {
reference_directories: vec![PathBuf::from("/home/rr/")],
..Default::default()
@ -1334,6 +1351,7 @@ mod tests {
let mut similar_images = SimilarImages {
similarity: 1,
common_data: CommonToolData {
tool_type: ToolType::SimilarImages,
use_reference_folders: false,
..Default::default()
},
@ -1358,6 +1376,7 @@ mod tests {
let mut similar_images = SimilarImages {
similarity: 4,
common_data: CommonToolData {
tool_type: ToolType::SimilarImages,
use_reference_folders: false,
..Default::default()
},
@ -1391,6 +1410,7 @@ mod tests {
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()
@ -1421,6 +1441,7 @@ mod tests {
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()
@ -1448,6 +1469,7 @@ mod tests {
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()
@ -1486,6 +1508,7 @@ mod tests {
let mut similar_images = SimilarImages {
similarity: 10,
common_data: CommonToolData {
tool_type: ToolType::SimilarImages,
directories: Directories {
reference_directories: vec![PathBuf::from("/home/rr/")],
..Default::default()

View file

@ -5,10 +5,9 @@ use std::mem;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use ffmpeg_cmdline_utils::FfmpegErrorKind::FfmpegNotFound;
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use humansize::{format_size, BINARY};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
@ -103,7 +102,7 @@ impl SimilarVideos {
}
#[fun_time(message = "find_similar_videos", level = "info")]
pub fn find_similar_videos(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) {
pub fn find_similar_videos(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
if !check_if_ffmpeg_is_installed() {
self.common_data.text_messages.errors.push(flc!("core_ffmpeg_not_found"));
#[cfg(target_os = "windows")]
@ -130,7 +129,7 @@ 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<&UnboundedSender<ProgressData>>) -> bool {
fn check_for_similar_videos(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let mut folders_to_check: Vec<PathBuf> = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector
if !self.common_data.allowed_extensions.using_custom_extensions() {
@ -266,7 +265,7 @@ impl SimilarVideos {
}
#[fun_time(message = "sort_videos", level = "debug")]
fn sort_videos(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn sort_videos(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let (loaded_hash_map, records_already_cached, non_cached_files_to_check) = self.load_cache_at_start();
let (progress_thread_handle, progress_thread_run, atomic_counter, check_was_stopped) =

View file

@ -6,9 +6,8 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use rayon::prelude::*;
use serde::Serialize;
@ -33,7 +32,7 @@ const TEMP_EXTENSIONS: &[&str] = &[
".partial",
];
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, Debug)]
pub struct FileEntry {
pub path: PathBuf,
pub modified_date: u64,
@ -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<&UnboundedSender<ProgressData>>) {
pub fn find_temporary_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) {
self.optimize_dirs_before_start();
if !self.check_files(stop_receiver, progress_sender) {
self.common_data.stopped_search = true;
@ -71,7 +70,7 @@ impl Temporary {
}
#[fun_time(message = "check_files", level = "debug")]
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&UnboundedSender<ProgressData>>) -> bool {
fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender<ProgressData>>) -> bool {
let mut folders_to_check: Vec<PathBuf> = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector
// Add root folders for finding
@ -153,9 +152,7 @@ impl Temporary {
) -> Option<FileEntry> {
atomic_counter.fetch_add(1, Ordering::Relaxed);
let Some(file_name_lowercase) = get_lowercase_name(entry_data, warnings) else {
return None;
};
let file_name_lowercase = get_lowercase_name(entry_data, warnings)?;
if !TEMP_EXTENSIONS.iter().any(|f| file_name_lowercase.ends_with(f)) {
return None;

View file

@ -3,7 +3,7 @@ name = "czkawka_gui"
version = "6.1.0"
authors = ["Rafał Mikrut <mikrutrafal@protonmail.com>"]
edition = "2021"
rust-version = "1.70.0"
rust-version = "1.72.1"
description = "GTK frontend of Czkawka"
license = "MIT"
homepage = "https://github.com/qarmin/czkawka"
@ -19,9 +19,6 @@ chrono = "0.4.31"
# Used for sending stop signal across threads
crossbeam-channel = "0.5.8"
# To get information about progress
futures = "0.3.28"
# For saving/loading config files to specific directories
directories-next = "2.0"
@ -46,7 +43,7 @@ fs_extra = "1.3"
# Language
i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.7"
rust-embed = "8.0"
rust-embed = { version = "8.0", features = ["debug-embed"] }
once_cell = "1.18"
log = "0.4.20"
@ -62,3 +59,4 @@ winapi = { version = "0.3.9", features = ["combaseapi", "objbase", "shobjidl_cor
[features]
default = []
heif = ["czkawka_core/heif"]
libraw = ["czkawka_core/libraw"]

91
czkawka_gui/README.md Normal file
View file

@ -0,0 +1,91 @@
# Czkawka GUI
Czkawka GUI is a graphical user interface for Czkawka Core written with GTK 4.
![Screenshot from 2023-11-26 12-43-32](https://github.com/qarmin/czkawka/assets/41945903/722ed490-0be1-4dac-bcfc-182a4d0787dc)
## Requirements
Requirements depends on platform that you are using.
Prebuild binareies are available here - https://github.com/qarmin/czkawka/releases/
### Linux
#### Prebuild binaries
Ubuntu - `sudo apt install libgtk-4 libheif libraw ffmpeg -y`
#### Snap -
none - all needed libraries are bundled in snap [except ffmpeg](https://github.com/snapcrafters/ffmpeg/issues/73) - https://snapcraft.io/czkawka
#### Flatpak
none - all needed libraries are bundled - https://flathub.org/apps/com.github.qarmin.czkawka
### Mac
```
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install gtk4 adwaita-icon-theme ffmpeg librsvg libheif libraw
```
### Windows
All needed libraries should be bundled in zip(except ffmpeg which you need download and unpack to location with `czkawka_gui.exe` - https://ffmpeg.org/download.html#build-windows)
You can also install app via msys2(webp and heif should work here) - https://www.msys2.org/#installation (czkawka package - https://packages.msys2.org/base/mingw-w64-czkawka)
```
pacman -S mingw-w64-x86_64-czkawka-gui
```
and you can create shortcut to `C:\msys64\mingw64\bin\czkawka_gui.exe`
## Compilation
Compilation of gui is harder that compilation cli or core, because uses gtk4 which is written in C and also requires a lot build and runtime dependencies.
### Requirements
| Program | Minimal version |
|:---------:|:-----------------:|
| Rust | 1.72.1 |
| GTK | 4.6 |
### Linux (Ubuntu, but on other OS should work similar)
```shell
sudo apt install libgtk-4-dev libheif-dev libraw-dev -y
cargo run --release --bin czkawka_gui
# Or with support for heif and libraw
cargo run --release --bin czkawka_gui --features "heif,libraw"
```
### Mac
```shell
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install rustup gtk4 adwaita-icon-theme ffmpeg librsvg libheif libraw pkg-config
rustup-init
cargo run --release --bin czkawka_gui
# Or with support for heif and libraw
cargo run --release --bin czkawka_gui --features "heif,libraw"
```
### Windows
Currently, there is no instruction how to compile app on Windows natively.</br>
You can check for CI for instructions how to cross-compile app from linux to windows(uses prebuilt docker image) - [CI Instructions](../.github/workflows/windows.yml)</br>
There exists mingw recipe which you can try to convert for your purposes - https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-czkawka/PKGBUILD
## Limitations
Not all available features other components implemented here, so this is list of limitations:
- Snap versions not allows to use similar videos feature
- Windows version not supports heif and webp files with prebuild binaries
- Prebuild binaries for mac arm not exists
## License
Code is distributed under MIT license.
Icon was created by [jannuary](https://github.com/jannuary) and licensed CC-BY-4.0.
Windows dark theme is used from project [WhiteSur](https://github.com/slypy/whitesur-gtk4-theme) with MIT license.
Some icons were taken from [ReShot](https://www.reshot.com) site and are licensed under Reshot Free License.
The program is completely free to use.
"Gratis to uczciwa cena" - "Free is a fair price"
## Name
Czkawka is a Polish word which means _hiccup_.
I chose this name because I wanted to hear people speaking other languages pronounce it, so feel free to spell it the way you want.
This name is not as bad as it seems, because I was also thinking about using words like _żółć_, _gżegżółka_ or _żołądź_,
but I gave up on these ideas because they contained Polish characters, which would cause difficulty in searching for the project.
At the beginning of the program creation, if the response concerning the name was unanimously negative, I prepared myself
for a possible change of the name of the program, and the opinions were extremely mixed.

View file

@ -544,7 +544,7 @@ move_files_title_dialog = Choose folder to which you want to move duplicated fil
move_files_choose_more_than_1_path = Only one path may be selected to be able to copy their duplicated files, selected {$path_number}.
move_stats = Properly moved {$num_files}/{$all_files} items
save_results_to_file = Saved results both to txt and json files.
save_results_to_file = Saved results both to txt and json files into {$name} folder.
search_not_choosing_any_music = ERROR: You must select at least one checkbox with music searching types.
search_not_choosing_any_broken_files = ERROR: You must select at least one checkbox with type of checked broken files.

View file

@ -1,5 +1,6 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::env;
use std::rc::Rc;
use gtk4::prelude::*;
@ -10,6 +11,7 @@ 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) {
@ -44,10 +46,15 @@ pub fn connect_button_save(gui_data: &GuiData) {
NotebookMainEnum::BadExtensions => shared_bad_extensions_state.borrow().save_all_in_one("results_bad_extensions"),
};
let current_path = match env::current_dir() {
Ok(t) => t.to_string_lossy().to_string(),
Err(_) => "<unknown>".to_string(),
};
match result {
Ok(()) => (),
Err(e) => {
entry_info.set_text(&format!("Failed to save results to file {e}"));
entry_info.set_text(&format!("Failed to save results to folder {current_path}, reason {e}"));
return;
}
}
@ -57,6 +64,7 @@ pub fn connect_button_save(gui_data: &GuiData) {
&shared_buttons,
&entry_info,
&buttons_save_clone,
current_path,
);
});
}
@ -66,8 +74,9 @@ fn post_save_things(
shared_buttons: &Rc<RefCell<HashMap<NotebookMainEnum, HashMap<BottomButtonsEnum, bool>>>>,
entry_info: &Entry,
buttons_save: &Button,
current_path: String,
) {
entry_info.set_text(&flg!("save_results_to_file"));
entry_info.set_text(&flg!("save_results_to_file", generate_translation_hashmap(vec![("name", current_path),])));
// Set state
{
buttons_save.hide();

View file

@ -3,16 +3,16 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use crossbeam_channel::Receiver;
use crossbeam_channel::{Receiver, Sender};
use fun_time::fun_time;
use futures::channel::mpsc::UnboundedSender;
use glib::Sender;
use glib::Sender as glibSender;
use gtk4::prelude::*;
use gtk4::Grid;
use czkawka_core::bad_extensions::BadExtensions;
use czkawka_core::big_file::BigFile;
use czkawka_core::broken_files::{BrokenFiles, CheckedTypes};
use czkawka_core::common::DEFAULT_THREAD_SIZE;
use czkawka_core::common_dir_traversal::{CheckingMethod, ProgressData};
use czkawka_core::common_tool::CommonData;
use czkawka_core::duplicate::DuplicateFinder;
@ -35,7 +35,7 @@ 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: Sender<Message>, progress_sender: UnboundedSender<ProgressData>) {
pub fn connect_button_search(gui_data: &GuiData, glib_stop_sender: glibSender<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();
@ -284,9 +284,9 @@ fn duplicate_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.show();
@ -316,90 +316,99 @@ fn duplicate_search(
let delete_outdated_cache = check_button_settings_duplicates_delete_outdated_cache.is_active();
// Find duplicates
thread::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();
});
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();
})
.unwrap();
}
fn empty_files_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.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);
// Find empty files
thread::spawn(move || {
let mut vf = EmptyFiles::new();
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut vf = 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();
});
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();
})
.unwrap();
}
fn empty_directories_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.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);
thread::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();
});
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();
})
.unwrap();
}
fn big_files_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.hide();
@ -413,55 +422,61 @@ fn big_files_search(
let numbers_of_files_to_check = entry_big_files_number.text().as_str().parse::<usize>().unwrap_or(50);
thread::spawn(move || {
let mut bf = BigFile::new();
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut bf = 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();
});
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();
})
.unwrap();
}
fn temporary_files_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.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);
thread::spawn(move || {
let mut tf = Temporary::new();
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut tf = 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();
});
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();
})
.unwrap();
}
fn same_music_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
show_dialog: &Arc<AtomicBool>,
) {
grid_progress_stages.show();
@ -510,28 +525,31 @@ fn same_music_search(
let minimum_segment_duration = scale_seconds_same_music.value() as f32;
if music_similarity != MusicSimilarity::NONE || check_method == CheckingMethod::AudioContent {
thread::spawn(move || {
let mut mf = SameMusic::new();
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut mf = 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();
});
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();
})
.unwrap();
} else {
let shared_buttons = gui_data.shared_buttons.clone();
let buttons_array = gui_data.bottom_buttons.buttons_array.clone();
@ -561,9 +579,9 @@ fn broken_files_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
show_dialog: &Arc<AtomicBool>,
) {
grid_progress_stages.show();
@ -592,21 +610,24 @@ fn broken_files_search(
}
if checked_types != CheckedTypes::NONE {
thread::spawn(move || {
let mut br = BrokenFiles::new();
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut br = 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();
});
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();
})
.unwrap();
} else {
let shared_buttons = gui_data.shared_buttons.clone();
let buttons_array = gui_data.bottom_buttons.buttons_array.clone();
@ -636,9 +657,9 @@ fn similar_image_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.show();
@ -669,38 +690,41 @@ fn similar_image_search(
let delete_outdated_cache = check_button_settings_similar_images_delete_outdated_cache.is_active();
thread::spawn(move || {
let mut sf = SimilarImages::new();
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut sf = 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();
});
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();
})
.unwrap();
}
fn similar_video_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.show();
@ -716,82 +740,91 @@ fn similar_video_search(
let ignore_same_size = check_button_video_ignore_same_size.is_active();
thread::spawn(move || {
let mut sf = SimilarVideos::new();
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut sf = 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();
});
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();
})
.unwrap();
}
fn bad_symlinks_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.hide();
let tree_view_invalid_symlinks = gui_data.main_notebook.tree_view_invalid_symlinks.clone();
clean_tree_view(&tree_view_invalid_symlinks);
thread::spawn(move || {
let mut isf = InvalidSymlinks::new();
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut isf = 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();
});
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();
})
.unwrap();
}
fn bad_extensions_search(
gui_data: &GuiData,
loaded_common_items: LoadedCommonItems,
stop_receiver: Receiver<()>,
glib_stop_sender: Sender<Message>,
glib_stop_sender: glibSender<Message>,
grid_progress_stages: &Grid,
progress_data_sender: UnboundedSender<ProgressData>,
progress_data_sender: Sender<ProgressData>,
) {
grid_progress_stages.show();
let tree_view_bad_extensions = gui_data.main_notebook.tree_view_bad_extensions.clone();
clean_tree_view(&tree_view_bad_extensions);
thread::spawn(move || {
let mut be = BadExtensions::new();
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut be = 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();
});
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();
})
.unwrap();
}
#[fun_time(message = "clean_tree_view", level = "debug")]

View file

@ -37,7 +37,7 @@ fn change_language(gui_data: &GuiData) {
pub fn load_system_language(gui_data: &GuiData) {
let requested_languages = DesktopLanguageRequester::requested_languages();
if let Some(language) = requested_languages.get(0) {
if let Some(language) = requested_languages.first() {
let old_short_lang = language.to_string();
let mut short_lang = String::new();
// removes from e.g. en_zb, ending _zd since Czkawka don't support this(maybe could add this in future, but only when)

View file

@ -1,9 +1,10 @@
use crossbeam_channel::Receiver;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use futures::channel::mpsc::UnboundedReceiver;
use futures::StreamExt;
use std::time::Duration;
use glib::MainContext;
use gtk4::prelude::*;
use gtk4::ProgressBar;
@ -19,29 +20,39 @@ use crate::taskbar_progress::tbp_flags::TBPF_INDETERMINATE;
use crate::taskbar_progress::TaskbarProgress;
#[allow(clippy::too_many_arguments)]
pub fn connect_progress_window(gui_data: &GuiData, mut progress_receiver: UnboundedReceiver<ProgressData>) {
pub fn connect_progress_window(gui_data: &GuiData, progress_receiver: Receiver<ProgressData>) {
let main_context = MainContext::default();
let _guard = main_context.acquire().unwrap();
let gui_data = gui_data.clone();
let future = async move {
while let Some(item) = progress_receiver.next().await {
match item.tool_type {
ToolType::Duplicate => process_bar_duplicates(&gui_data, &item),
ToolType::EmptyFiles => process_bar_empty_files(&gui_data, &item),
ToolType::EmptyFolders => process_bar_empty_folder(&gui_data, &item),
ToolType::BigFile => process_bar_big_files(&gui_data, &item),
ToolType::SameMusic => process_bar_same_music(&gui_data, &item),
ToolType::SimilarImages => process_bar_similar_images(&gui_data, &item),
ToolType::SimilarVideos => process_bar_similar_videos(&gui_data, &item),
ToolType::TemporaryFiles => process_bar_temporary(&gui_data, &item),
ToolType::InvalidSymlinks => process_bar_invalid_symlinks(&gui_data, &item),
ToolType::BrokenFiles => process_bar_broken_files(&gui_data, &item),
ToolType::BadExtensions => process_bar_bad_extensions(&gui_data, &item),
ToolType::None => panic!(),
loop {
loop {
let item = progress_receiver.try_recv();
if let Ok(item) = item {
match item.tool_type {
ToolType::Duplicate => process_bar_duplicates(&gui_data, &item),
ToolType::EmptyFiles => process_bar_empty_files(&gui_data, &item),
ToolType::EmptyFolders => process_bar_empty_folder(&gui_data, &item),
ToolType::BigFile => process_bar_big_files(&gui_data, &item),
ToolType::SameMusic => process_bar_same_music(&gui_data, &item),
ToolType::SimilarImages => process_bar_similar_images(&gui_data, &item),
ToolType::SimilarVideos => process_bar_similar_videos(&gui_data, &item),
ToolType::TemporaryFiles => process_bar_temporary(&gui_data, &item),
ToolType::InvalidSymlinks => process_bar_invalid_symlinks(&gui_data, &item),
ToolType::BrokenFiles => process_bar_broken_files(&gui_data, &item),
ToolType::BadExtensions => process_bar_bad_extensions(&gui_data, &item),
ToolType::None => panic!(),
}
} else {
break;
}
}
glib::timeout_future(Duration::from_millis(300)).await;
}
};
main_context.spawn_local(future);
}

View file

@ -14,6 +14,7 @@ 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;
@ -29,11 +30,6 @@ use crate::flg;
use crate::notebook_enums::{NotebookMainEnum, NotebookUpperEnum};
use crate::notebook_info::{NotebookObject, NOTEBOOKS_INFO};
#[cfg(not(target_family = "windows"))]
pub const CHARACTER: char = '/';
#[cfg(target_family = "windows")]
pub const CHARACTER: char = '\\';
pub const KEY_DELETE: u32 = 119;
pub const KEY_ENTER: u32 = 36;
pub const KEY_SPACE: u32 = 65;

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 futures::channel::mpsc;
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
use glib::Priority;
use gtk4::gio::ApplicationFlags;
use gtk4::prelude::*;
@ -87,7 +86,7 @@ fn build_ui(application: &Application, arguments: &[OsString]) {
let (glib_stop_sender, glib_stop_receiver) = glib::MainContext::channel(Priority::default());
// Futures progress report
let (progress_sender, progress_receiver): (UnboundedSender<ProgressData>, UnboundedReceiver<ProgressData>) = mpsc::unbounded();
let (progress_sender, progress_receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();
initialize_gui(&gui_data);
validate_notebook_data(&gui_data); // Must be run after initialization of gui, to check if everything was properly setup

View file

@ -6,7 +6,7 @@ use crate::GuiData;
pub fn validate_notebook_data(gui_data: &GuiData) {
// Test treeviews names, each treeview should have set name same as variable name
for (_i, item) in gui_data.main_notebook.get_main_tree_views().iter().enumerate() {
for item in &gui_data.main_notebook.get_main_tree_views() {
// println!("Checking {} element", i);
get_notebook_enum_from_tree_view(item);

View file

@ -1,14 +0,0 @@
[package]
name = "czkawka_slint"
version = "6.1.0"
authors = ["Rafał Mikrut <mikrutrafal@protonmail.com>"]
edition = "2021"
rust-version = "1.72.1"
description = "Slint frontend of Czkawka"
license = "GPL-3"
homepage = "https://github.com/qarmin/czkawka"
repository = "https://github.com/qarmin/czkawka"
[dependencies]
slint = "1.2.2"
rand = "0.8.5"

View file

@ -1,3 +0,0 @@
fn main() {}
slint::slint! {}

View file

@ -1,5 +1,12 @@
# Compiling Czkawka from sources
This instruction is outdated and will be removed in one of next version, please look at README.md files in each module folder for more up to date instructions.
- [Czkawka GUI (GTK frontend)](../czkawka_gui/README.md)</br>
- [Czkawka CLI](../czkawka_cli/README.md)</br>
- [Czkawka Core](../czkawka_core/README.md)</br>
- [Krokiet GUI (Slint frontend)](../krokiet/README.md)</br>
## Requirements
If you only want the terminal version without a GUI, just skip all the packages with `gtk` in their names.
@ -12,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.70.0 | The minimum version of rust does not depend on anything, so it can change frequently |
| Rust | 1.72.1 | 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

@ -1,4 +1,11 @@
# Installation
This instruction is outdated and will be removed in one of next version, please look at README.md files in each module folder for more up to date instructions.
- [Czkawka GUI (GTK frontend)](../czkawka_gui/README.md)</br>
- [Czkawka CLI](../czkawka_cli/README.md)</br>
- [Czkawka Core](../czkawka_core/README.md)</br>
- [Krokiet GUI (Slint frontend)](../krokiet/README.md)</br>
## Requirements
### Linux
If you use Snap, Flatpak or Appimage, you need to only install ffmpeg if you want to use Similar Videos tool.
@ -140,7 +147,7 @@ Flathub page with Czkawka can be found [**here**](https://flathub.org/apps/detai
### PPA - Debian/Ubuntu (unofficial)
```
sudo add-apt-repository ppa:xtradeb/apps
sudo apt-get update
sudo apt update
sudo apt-get install czkawka
```

View file

@ -8,6 +8,10 @@ Main/Default language is English, but also Polish is officially supported.
Translating is mostly done by site - https://crowdin.com/project/czkawka
If you want to translate Czkawka to your language, you can do it in this site.
Next chapters are only for internal use, so just use crowdin page.
## How to translate Czkawka?
Base translatable strings are placed under `i18n/en/czkawka_gui.ftl` file.

0
krokiet/.clippy.toml Normal file
View file

63
krokiet/Cargo.toml Normal file
View file

@ -0,0 +1,63 @@
[package]
name = "krokiet"
version = "6.1.0"
authors = ["Rafał Mikrut <mikrutrafal@protonmail.com>"]
edition = "2021"
rust-version = "1.72.1"
description = "Slint frontend of Czkawka Core"
license = "GPL-3"
homepage = "https://github.com/qarmin/czkawka"
repository = "https://github.com/qarmin/czkawka"
build = "build.rs"
[dependencies]
# 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 = [
"std",
"backend-winit",
"compat-1-2"
] }
rand = "0.8"
czkawka_core = { version = "6.1.0", path = "../czkawka_core" }
chrono = "0.4.31"
open = "5.0"
crossbeam-channel = "0.5.8"
handsome_logger = "0.8"
rfd = { version = "0.12", default-features = false, features = ["xdg-portal"] }
home = "0.5"
log = "0.4.20"
serde = "1.0"
serde_json = "1.0"
humansize = "2.1"
image = "0.24"
directories-next = "2.0"
image_hasher = "1.2"
rayon = "1.8.0"
# Translations
i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] }
i18n-embed-fl = "0.7"
rust-embed = { version = "8.0", features = ["debug-embed"] }
once_cell = "1.18"
[build-dependencies]
slint-build = "1.3"
#slint-build = { git = "https://github.com/slint-ui/slint.git" }
#slint-build = { path = "/home/rafal/test/slint/api/rs/build/"}
[features]
default = ["winit_femtovg", "winit_software"]
skia_opengl = ["slint/renderer-skia-opengl"]
skia_vulkan = ["slint/renderer-skia-vulkan"]
software = ["slint/renderer-software"]
femtovg = ["slint/renderer-femtovg"]
winit_femtovg = ["slint/renderer-winit-femtovg"]
winit_skia_opengl = ["slint/renderer-winit-skia-opengl"]
winit_skia_vulkan = ["slint/renderer-winit-skia-vulkan"]
winit_software = ["slint/renderer-winit-software"]
heif = ["czkawka_core/heif"]
libraw = ["czkawka_core/libraw"]

21
krokiet/LICENSE_MIT_CODE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2023 Rafał Mikrut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

160
krokiet/README.md Normal file
View file

@ -0,0 +1,160 @@
# Krokiet
Krokiet is new Czkawka frontend written in Slint(written mostly in 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
frontend(but of course I want implement most of features from other project).
## Usage
Krokiet should not have any special runtime requirements - it should work on almost any device non-antic device.
Prebuild binaries should work on Windows 10,11, Mac, Ubuntu 22.04/20.04 and similar(libheif version + czkawka_gui requires Ubuntu 22.04+ and rest Ubuntu 20.04) - https://github.com/qarmin/czkawka/releases/
## Compilation
On Ubuntu you need to install this dependencies:
```
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,
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
use it, that require on windows msvc compiler(not sure how to exactly install it).
Also skia renderer is written in C++ and uses on platforms like x86_64 and arm64 prebuild binaries, so if you are using
different architecture, this library will be build from source, which can take a lot of time and require additional
dependencies.
## Additional Renderers
By default, only femtovg(opengl) and software renderer are enabled, but you can enable more renderers by compiling app
with additional features.
Most of the users will want to use app with windowing system/compositor, so features starting with `winit` in name are
recommended.
E.g.
```
cargo build --release --features "winit_skia_opengl"
cargo build --release --features "winit_software"
```
to run app with different renderers you need to use it, by adding `SLINT_BACKEND` environment
```
SLINT_BACKEND=winit-femtovg ./target/release/krokiet
SLINT_BACKEND=software ./target/release/krokiet
SLINT_BACKEND=skia ./target/release/krokiet # This uses now opengl - https://github.com/slint-ui/slint/discussions/3799
```
when you will use invalid/non-existing backend, app will show warning
```
slint winit: unrecognized renderer skia, falling back to FemtoVG
```
to check what is really used, add `SLINT_DEBUG_PERFORMANCE=refresh_lazy,console,overlay` env
```
SLINT_DEBUG_PERFORMANCE=refresh_lazy,console,overlay cargo run
```
should print something like
```
Slint: Build config: debug; Backend: software
```
## Different theme
App was created with dark fluent theme in mind, but is possible to use light theme by setting `SLINT_STYLE` environment
variable to `fluent-light` during compilation e.g.
```
SLINT_STYLE=fluent-light cargo run -- --path .
```
Slint supports also other themes, but they are not officially supported by this app and may be broken(but looks that
cupertino looks quite good with current style).
```
SLINT_STYLE=cupertino-light cargo run -- --path .
SLINT_STYLE=cupertino-dark cargo run -- --path .
SLINT_STYLE=material-light cargo run -- --path .
SLINT_STYLE=material-dark cargo run -- --path .
```
## How to help?
- Suggesting possible design changes in the gui - of course, they should be possible to be simply implemented in the
slint keeping in mind the performance aspect as well
- Modifying user interface - gui is written in simple language similar to qml, that can be modified in vscode/web with
live
preview - [slint live preview example](https://slint.dev/releases/1.3.0/editor/?load_demo=examples/printerdemo/ui/printerdemo.slint)
- 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.
| Toolkit | Pros | Cons |
|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Gtk 4 | - Hard compilation/cross compilation and bundling all required libraries - mostly on windows </br> - Cambalache can be used to create graphically gui </br> - Good gtk4-rs bindings(but sometimes not really intuitive) | - Hard compilation/cross compilation and bundling all required libraries - mostly on windows </br> - Forcing the use of a specific gui creation style </br> - Strange crashes, not working basic features, etc.(again, mostly on windows) </br> - Forcing to use bugged/outdated but dynamically loaded version of libraries on linux (e.g. 4.6 on Ubuntu 22.04) - not all fixes are backported |
| Qt | - QML support - simplify creating of gui from code it is easy to use and powerful </br> - Very flexible framework <br/> - Typescript/javascript <=> qml interoperability </br> - Probably the most mature GUI library | - New and limited qt bindings <br/> - Hard to cross-compile <br/> - Very easy to create and use invalid state in QML(unexpected null/undefined values, messed properties bindings etc.) <br/> - Commercial license or GPL |
| Slint | - Internal language is compiled to native code <br/> - Live gui preview with Vscode/Vscodium without needing to use rust <br/> - Full rust solution - easy to compile/cross compile, minimal runtime requirements </br> - Static type checks in slint files | - Internal .slint language is more limited than QML <br/> - Out of bounds and similar errors are quietly being corrected instead printing error - this can lead to hard to debug problems <br/> - Only GPL is only available open-source license <br/> - Popup windows almost not exists <br/> - Internal widgets are almost not customizable and usually quite limited |
| Iced | - ~100% rust code - so compilation is simple </br> - Elm architecture - simple to understand | - Mostly maintained by one person - slows down fixing bugs and implementing new features </br> - GUI can be created only from rust code, which really is bad for creating complex GUIs(mostly due rust compile times) </br> - Docs are almost non-existent |
| Tauri | - Easy to create ui(at least for web developers) - uses html/css/js</br>- Quite portable | - Webview dependency - it is not really lightweight and can be hard to compile on some platforms and on Linux e.g. webRTC not working</br>- Cannot select directory - file chooser only can choose files - small thing but important for me</br>- Not very performant Rust <=> Javascript communication |
Since I don't have time to create really complex and good looking GUI, I needed a helper tool to create GUI not from
Rust(I don't want to use different language, because this will make communication with czkawka_core harder) so I decided
to not look at Iced which only allows to create GUI from Rust.
GTK and QT also I throw away due cross compilation problems caused mostly by using C/C++ internally. Using GTK in
Czkawka was a reason why I started to find other toolkits.
Tauri - I don't really like to use Javascript because I already used it with Qt(C++) + QML + Typescript combination and
I found that creating ui in such language may be simple at start but later any bigger changes cause a lot of runtime
errors.
So only Slint left with its cons and pros.
## License
Code is licensed under MIT license but entire project is licensed under GPL-3.0 license, due Slint license restrictions.
## Name
Why Krokiet(eng. Croquette)?
Because I like croquettes(Polish version), the ones with meat, mushrooms wrapped in breadcrumbs... it makes my mouth
water.
I considered also other dishes which I like to eat like pierogi, żurek, pączek, schabowy or zapiekanka.
This name should be a lot of easier to remember than czkawka or szyszka.

9
krokiet/build.rs Normal file
View file

@ -0,0 +1,9 @@
use std::env;
fn main() {
if env::var("SLINT_STYLE").is_err() || env::var("SLINT_STYLE") == Ok(String::new()) {
slint_build::compile_with_config("ui/main_window.slint", slint_build::CompilerConfiguration::new().with_style("fluent-dark".into())).unwrap();
} else {
slint_build::compile("ui/main_window.slint").unwrap();
}
}

13
krokiet/i18n.toml Normal file
View file

@ -0,0 +1,13 @@
# (Required) The language identifier of the language used in the
# source code for gettext system, and the primary fallback language
# (for which all strings must be present) when using the fluent
# system.
fallback_language = "en"
# Use the fluent localization system.
[fluent]
# (Required) The path to the assets directory.
# The paths inside the assets directory should be structured like so:
# `assets_dir/{language}/{domain}.ftl`
assets_dir = "i18n"

View file

@ -0,0 +1 @@
settings_language = Languages

BIN
krokiet/icons/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1 @@
<svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><path d="m39.6 27.2c.1-.7.2-1.4.2-2.2s-.1-1.5-.2-2.2l4.5-3.2c.4-.3.6-.9.3-1.4l-4.4-7.4c-.3-.5-.8-.7-1.3-.4l-5 2.3c-1.2-.9-2.4-1.6-3.8-2.2l-.5-5.5c-.1-.5-.5-.9-1-.9h-8.6c-.5 0-1 .4-1 .9l-.5 5.5c-1.4.6-2.7 1.3-3.8 2.2l-5-2.3c-.5-.2-1.1 0-1.3.4l-4.3 7.4c-.3.5-.1 1.1.3 1.4l4.5 3.2c-.1.7-.2 1.4-.2 2.2s.1 1.5.2 2.2l-4.7 3.2c-.4.3-.6.9-.3 1.4l4.3 7.4c.3.5.8.7 1.3.4l5-2.3c1.2.9 2.4 1.6 3.8 2.2l.5 5.5c.1.5.5.9 1 .9h8.6c.5 0 1-.4 1-.9l.5-5.5c1.4-.6 2.7-1.3 3.8-2.2l5 2.3c.5.2 1.1 0 1.3-.4l4.3-7.4c.3-.5.1-1.1-.3-1.4zm-15.6 7.8c-5.5 0-10-4.5-10-10s4.5-10 10-10 10 4.5 10 10-4.5 10-10 10z" fill="#607d8b"/><path d="m24 13c-6.6 0-12 5.4-12 12s5.4 12 12 12 12-5.4 12-12-5.4-12-12-12zm0 17c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5z" fill="#455a64"/></svg>

After

Width:  |  Height:  |  Size: 833 B

55
krokiet/src/common.rs Normal file
View file

@ -0,0 +1,55 @@
use crate::CurrentTab;
use slint::{ModelRc, SharedString, StandardListViewItem, VecModel};
use std::path::PathBuf;
// 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 {
match active_tab {
CurrentTab::EmptyFolders => 1,
CurrentTab::EmptyFiles => 1,
CurrentTab::SimilarImages => 4,
CurrentTab::Settings => panic!("Button should be disabled"),
}
}
pub fn get_name_idx(active_tab: CurrentTab) -> usize {
match active_tab {
CurrentTab::EmptyFolders => 0,
CurrentTab::EmptyFiles => 0,
CurrentTab::SimilarImages => 3,
CurrentTab::Settings => panic!("Button should be disabled"),
}
}
pub fn get_is_header_mode(active_tab: CurrentTab) -> bool {
match active_tab {
CurrentTab::EmptyFolders | CurrentTab::EmptyFiles => false,
CurrentTab::SimilarImages => true,
CurrentTab::Settings => panic!("Button should be disabled"),
}
}
// pub fn create_string_standard_list_view(items: &[String]) -> ModelRc<StandardListViewItem> {
// let new_folders_standard_list_view = items
// .iter()
// .map(|x| {
// let mut element = StandardListViewItem::default();
// element.text = x.into();
// element
// })
// .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
.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_vec_model_from_vec_string(items: Vec<String>) -> VecModel<SharedString> {
VecModel::from(items.into_iter().map(SharedString::from).collect::<Vec<_>>())
}

View file

@ -0,0 +1,276 @@
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 log::info;
use rayon::prelude::*;
pub fn connect_delete_button(app: &MainWindow) {
let a = app.as_weak();
app.global::<Callabler>().on_delete_selected_items(move || {
let app = a.upgrade().unwrap();
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 new_model = handle_delete_items(&model, active_tab);
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"),
}
}
app.global::<GuiState>().set_preview_visible(false);
});
}
fn handle_delete_items(items: &ModelRc<MainListModel>, active_tab: CurrentTab) -> 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);
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);
}
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<_>>();
info!("Removing items: {:?} {:?}", items_to_remove, active_tab);
// 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);
});
} 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;
}
}
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)
}
}

View file

@ -0,0 +1,121 @@
use rfd::FileDialog;
use slint::{ComponentHandle, Model, ModelRc, VecModel};
use crate::{Callabler, MainWindow, Settings};
pub fn connect_add_remove_directories(app: &MainWindow) {
connect_add_directories(app);
connect_remove_directories(app);
connect_add_manual_directories(app);
}
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() {
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)));
} 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)));
}
});
}
fn connect_remove_directories(app: &MainWindow) {
let a = app.as_weak();
app.global::<Callabler>().on_remove_item_directories(move |included_directories, current_index| {
// 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 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)));
}
} else {
let excluded_model = settings.get_excluded_directories();
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)));
}
}
});
}
fn connect_add_directories(app: &MainWindow) {
let a = app.as_weak();
app.on_folder_choose_requested(move |included_directories| {
let app = a.upgrade().unwrap();
let directory = std::env::current_dir().unwrap_or(std::path::PathBuf::from("/"));
let file_dialog = FileDialog::new().set_directory(directory);
let Some(folders) = file_dialog.pick_folders() else {
return;
};
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);
} else {
settings.set_excluded_directories(new_folders_model);
}
});
}

View file

@ -0,0 +1,39 @@
use crate::{Callabler, MainWindow};
use directories_next::ProjectDirs;
use log::error;
use slint::ComponentHandle;
pub fn connect_open_items(app: &MainWindow) {
app.global::<Callabler>().on_item_opened(move |path| {
match open::that(&*path) {
Ok(()) => {}
Err(e) => {
eprintln!("Failed to open file: {e}");
}
};
// TODO - this should be added to line edit
});
app.global::<Callabler>().on_open_config_folder(move || {
let Some(dirs) = ProjectDirs::from("pl", "Qarmin", "Krokiet") else {
error!("Failed to open config folder");
return;
};
let config_folder = dirs.config_dir();
if let Err(e) = open::that(config_folder) {
error!("Failed to open config folder {:?}: {e}", config_folder);
}
});
// Cache uses Czkawka name to easily change between apps
app.global::<Callabler>().on_open_cache_folder(move || {
let Some(dirs) = ProjectDirs::from("pl", "Qarmin", "Czkawka") else {
error!("Failed to open cache folder");
return;
};
let cache_folder = dirs.cache_dir();
if let Err(e) = open::that(cache_folder) {
error!("Failed to open cache folder {:?}: {e}", cache_folder);
}
});
}

View file

@ -0,0 +1,93 @@
use crate::{MainWindow, ProgressToSend};
use crossbeam_channel::Receiver;
use czkawka_core::common_dir_traversal::{ProgressData, ToolType};
use slint::ComponentHandle;
use std::thread;
pub fn connect_progress_gathering(app: &MainWindow, progress_receiver: Receiver<ProgressData>) {
let a = app.as_weak();
thread::spawn(move || loop {
let Ok(progress_data) = progress_receiver.recv() else {
return; // Channel closed, so exit the thread since app closing
};
a.upgrade_in_event_loop(move |app| {
let to_send;
match progress_data.tool_type {
ToolType::EmptyFiles => {
let (all_progress, current_progress) = no_current_stage_get_data(&progress_data);
to_send = ProgressToSend {
all_progress,
current_progress,
step_name: format!("Checked {} files", progress_data.entries_checked).into(),
};
}
ToolType::EmptyFolders => {
let (all_progress, current_progress) = no_current_stage_get_data(&progress_data);
to_send = ProgressToSend {
all_progress,
current_progress,
step_name: format!("Checked {} folders", progress_data.entries_checked).into(),
};
}
ToolType::SimilarImages => {
let step_name;
let all_progress;
let current_progress;
match progress_data.current_stage {
0 => {
(all_progress, current_progress) = no_current_stage_get_data(&progress_data);
step_name = format!("Scanning {} file", progress_data.entries_checked);
}
1 => {
(all_progress, current_progress) = common_get_data(&progress_data);
step_name = format!("Hashing {}/{} image", progress_data.entries_checked, progress_data.entries_to_check);
}
2 => {
(all_progress, current_progress) = common_get_data(&progress_data);
step_name = format!("Comparing {}/{} image hash", progress_data.entries_checked, progress_data.entries_to_check);
}
_ => panic!(),
}
to_send = ProgressToSend {
all_progress,
current_progress,
step_name: step_name.into(),
};
}
_ => {
panic!("Invalid tool type {:?}", progress_data.tool_type);
}
}
app.set_progress_datas(to_send);
})
.unwrap();
});
}
// Used when current stage not have enough data to show status, so we show only all_stages
// Happens if we searching files and we don't know how many files we need to check
fn no_current_stage_get_data(item: &ProgressData) -> (i32, i32) {
let all_stages = (item.current_stage as f64) / (item.max_stage + 1) as f64;
((all_stages * 100.0) as i32, -1)
}
// Used to calculate number of files to check and also to calculate current progress according to number of files to check and checked
fn common_get_data(item: &ProgressData) -> (i32, i32) {
if item.entries_to_check != 0 {
let all_stages = (item.current_stage as f64 + (item.entries_checked) as f64 / item.entries_to_check as f64) / (item.max_stage + 1) as f64;
let all_stages = if all_stages > 0.99 { 0.99 } else { all_stages };
let current_stage = (item.entries_checked) as f64 / item.entries_to_check as f64;
let current_stage = if current_stage > 0.99 { 0.99 } else { current_stage };
((all_stages * 100.0) as i32, (current_stage * 100.0) as i32)
} else {
let all_stages = (item.current_stage as f64) / (item.max_stage + 1) as f64;
let all_stages = if all_stages > 0.99 { 0.99 } else { all_stages };
((all_stages * 100.0) as i32, 0)
}
}

208
krokiet/src/connect_scan.rs Normal file
View file

@ -0,0 +1,208 @@
use crate::settings::{collect_settings, SettingsCustom, ALLOWED_HASH_TYPE_VALUES, ALLOWED_RESIZE_ALGORITHM_VALUES};
use crate::{CurrentTab, GuiState, MainListModel, MainWindow, ProgressToSend};
use chrono::NaiveDateTime;
use crossbeam_channel::{Receiver, Sender};
use czkawka_core::common::{split_path, DEFAULT_THREAD_SIZE};
use czkawka_core::common_dir_traversal::ProgressData;
use czkawka_core::common_tool::CommonData;
use czkawka_core::common_traits::ResultEntry;
use czkawka_core::empty_files::EmptyFiles;
use czkawka_core::empty_folder::EmptyFolder;
use czkawka_core::similar_images;
use czkawka_core::similar_images::SimilarImages;
use humansize::{format_size, BINARY};
use slint::{ComponentHandle, ModelRc, SharedString, VecModel, Weak};
use std::path::PathBuf;
use std::rc::Rc;
use std::thread;
pub fn connect_scan_button(app: &MainWindow, progress_sender: Sender<ProgressData>, stop_receiver: Receiver<()>) {
let a = app.as_weak();
app.on_scan_starting(move |active_tab| {
let progress_sender = progress_sender.clone();
let stop_receiver = stop_receiver.clone();
let app = a.upgrade().unwrap();
app.set_progress_datas(ProgressToSend {
all_progress: 0,
current_progress: -1,
step_name: "".into(),
});
let custom_settings = collect_settings(&app);
let a = app.as_weak();
match active_tab {
CurrentTab::EmptyFolders => {
scan_empty_folders(a, progress_sender, stop_receiver, custom_settings);
}
CurrentTab::EmptyFiles => {
scan_empty_files(a, progress_sender, stop_receiver, custom_settings);
}
CurrentTab::SimilarImages => {
scan_similar_images(a, progress_sender, stop_receiver, custom_settings);
}
CurrentTab::Settings => panic!("Button should be disabled"),
}
});
}
// 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)
.spawn(move || {
let mut finder = SimilarImages::new();
set_common_settings(&mut finder, &custom_settings);
finder.set_hash_size(custom_settings.similar_images_sub_hash_size);
let resize_algortithm = ALLOWED_RESIZE_ALGORITHM_VALUES
.iter()
.find(|(setting_name, _gui_name, _resize_alg)| setting_name == &custom_settings.similar_images_sub_resize_algorithm)
.expect("Resize algorithm not found")
.2;
finder.set_image_filter(resize_algortithm);
let hash_type = ALLOWED_HASH_TYPE_VALUES
.iter()
.find(|(setting_name, _gui_name, _resize_alg)| setting_name == &custom_settings.similar_images_sub_hash_type)
.expect("Hash type not found")
.2;
finder.set_hash_alg(hash_type);
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();
for vec_fe in &mut vector {
vec_fe.sort_unstable_by_key(|e| e.similarity);
}
let hash_size = finder.hash_size;
a.upgrade_in_event_loop(move |app| {
let number_of_empty_files = vector.len();
let items = Rc::new(VecModel::default());
for vec_fe in vector {
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(),
]);
insert_data_to_model(&items, data_model, false);
}
}
app.set_similar_images_model(items.into());
app.invoke_scan_ended(format!("Found {} similar images files", number_of_empty_files).into());
app.global::<GuiState>().set_info_text(messages.into());
})
})
.unwrap();
}
fn scan_empty_files(a: Weak<MainWindow>, progress_sender: Sender<ProgressData>, stop_receiver: Receiver<()>, custom_settings: SettingsCustom) {
thread::Builder::new()
.stack_size(DEFAULT_THREAD_SIZE)
.spawn(move || {
let mut finder = EmptyFiles::new();
set_common_settings(&mut finder, &custom_settings);
finder.find_empty_files(Some(&stop_receiver), Some(&progress_sender));
let mut vector = finder.get_empty_files().clone();
let messages = finder.get_text_messages().create_messages_text();
vector.sort_unstable_by_key(|e| {
let t = split_path(e.get_path());
(t.0, t.1)
});
a.upgrade_in_event_loop(move |app| {
let number_of_empty_files = 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);
}
app.set_empty_files_model(items.into());
app.invoke_scan_ended(format!("Found {} empty files", number_of_empty_files).into());
app.global::<GuiState>().set_info_text(messages.into());
})
})
.unwrap();
}
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)
.spawn(move || {
let mut finder = EmptyFolder::new();
set_common_settings(&mut finder, &custom_settings);
finder.find_empty_folders(Some(&stop_receiver), Some(&progress_sender));
let mut vector = finder.get_empty_folder_list().keys().cloned().collect::<Vec<PathBuf>>();
let messages = finder.get_text_messages().create_messages_text();
vector.sort_unstable_by_key(|e| {
let t = split_path(e.as_path());
(t.0, t.1)
});
a.upgrade_in_event_loop(move |app| {
let folder_map = finder.get_empty_folder_list();
let items = Rc::new(VecModel::default());
for path in vector {
let (directory, file) = split_path(&path);
let data_model = VecModel::from_slice(&[
file.into(),
directory.into(),
NaiveDateTime::from_timestamp_opt(folder_map[&path].modified_date as i64, 0).unwrap().to_string().into(),
]);
insert_data_to_model(&items, data_model, false);
}
app.set_empty_folder_model(items.into());
app.invoke_scan_ended(format!("Found {} empty folders", folder_map.len()).into());
app.global::<GuiState>().set_info_text(messages.into());
})
})
.unwrap();
}
fn insert_data_to_model(items: &Rc<VecModel<MainListModel>>, data_model: ModelRc<SharedString>, header_row: bool) {
let main = MainListModel {
checked: false,
header_row,
selected_row: false,
val: ModelRc::new(data_model),
};
items.push(main);
}
fn set_common_settings<T>(component: &mut T, custom_settings: &SettingsCustom)
where
T: CommonData,
{
component.set_included_directory(custom_settings.included_directories.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_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);
component.set_save_also_as_json(custom_settings.save_also_as_json);
}

View file

@ -0,0 +1,82 @@
use crate::{Callabler, GuiState, MainWindow};
use czkawka_core::common::{get_dynamic_image_from_raw_image, IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS};
use image::DynamicImage;
use log::{debug, error};
use slint::ComponentHandle;
use std::path::Path;
use std::time::{Duration, Instant};
pub type ImageBufferRgba = image::ImageBuffer<image::Rgba<u8>, Vec<u8>>;
pub fn connect_show_preview(app: &MainWindow) {
let a = app.as_weak();
app.global::<Callabler>().on_load_image_preview(move |image_path| {
let app = a.upgrade().unwrap();
let path = Path::new(image_path.as_str());
let res = load_image(path);
if let Some((load_time, img)) = res {
let start_timer_convert_time = Instant::now();
let slint_image = convert_into_slint_image(img);
let convert_time = start_timer_convert_time.elapsed();
let start_set_time = Instant::now();
app.global::<GuiState>().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);
} else {
app.global::<GuiState>().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());
slint::Image::from_rgba8(buffer)
}
fn load_image(image_path: &Path) -> Option<(Duration, image::DynamicImage)> {
if !image_path.is_file() {
return None;
}
let image_name = image_path.to_string_lossy().to_string();
let image_extension = image_path.extension()?.to_string_lossy().to_lowercase();
let extension_with_dot = format!(".{}", image_extension);
let is_raw_image = RAW_IMAGE_EXTENSIONS.contains(&extension_with_dot.as_str());
let is_normal_image = IMAGE_RS_EXTENSIONS.contains(&extension_with_dot.as_str());
if !is_raw_image && !is_normal_image {
return None;
}
let load_img_start_timer = Instant::now();
// TODO this needs to be run inside closure
let img = if is_normal_image {
match image::open(image_name) {
Ok(img) => img,
Err(e) => {
error!("Error while loading image: {}", e);
return None;
}
}
} else if is_raw_image {
if let Some(img) = get_dynamic_image_from_raw_image(image_name) {
img
} else {
error!("Error while loading raw image - not sure why - try to guess");
return None;
}
} else {
panic!("Used not supported image extension");
};
Some((load_img_start_timer.elapsed(), img))
}

View file

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

View file

@ -0,0 +1,23 @@
use crate::localizer_krokiet::LANGUAGE_LOADER_GUI;
use crate::{Callabler, MainWindow};
use slint::ComponentHandle;
use slint::Model;
use std::collections::HashMap;
pub fn connect_translations(app: &MainWindow) {
app.global::<Callabler>().on_translate(move |text_to_translate, args| {
let text_to_translate = text_to_translate.to_string();
let mut arguments = HashMap::new();
args.iter().for_each(|(key, value)| {
arguments.insert(key.to_string(), value.to_string());
});
if arguments.is_empty() {
LANGUAGE_LOADER_GUI.get(&text_to_translate)
} else {
LANGUAGE_LOADER_GUI.get_args(&text_to_translate, arguments)
}
.into()
});
}

View file

@ -0,0 +1,32 @@
use i18n_embed::fluent::{fluent_language_loader, FluentLanguageLoader};
use i18n_embed::LanguageLoader;
use once_cell::sync::Lazy;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "i18n/"]
struct Localizations;
pub static LANGUAGE_LOADER_GUI: Lazy<FluentLanguageLoader> = Lazy::new(|| {
let loader: FluentLanguageLoader = fluent_language_loader!();
loader.load_fallback_language(&Localizations).expect("Error while loading fallback language");
loader
});
#[macro_export]
macro_rules! flk {
($message_id:literal) => {{
i18n_embed_fl::fl!($crate::localizer_krokiet::LANGUAGE_LOADER_GUI, $message_id)
}};
($message_id:literal, $($args:expr),*) => {{
i18n_embed_fl::fl!($crate::localizer_krokiet::LANGUAGE_LOADER_GUI, $message_id, $($args), *)
}};
}
// // Get the `Localizer` to be used for localizing this library.
// pub fn localizer_krokiet() -> Box<dyn Localizer> {
// Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER_GUI, &Localizations))
// }

134
krokiet/src/main.rs Normal file
View file

@ -0,0 +1,134 @@
// Remove console window in Windows OS
#![windows_subsystem = "windows"]
#![allow(unknown_lints)] // May be disabled, but locally I use nightly clippy
#![allow(clippy::comparison_chain)]
#![allow(clippy::collapsible_if)]
#![allow(clippy::should_panic_without_expect)]
#![allow(clippy::struct_field_names)] // Generated code
#![allow(clippy::overly_complex_bool_expr)] // Generated code
#![allow(clippy::semicolon_if_nothing_returned)] // Generated code
#![allow(clippy::used_underscore_binding)] // Generated code
#![allow(clippy::unreadable_literal)] // Generated code
#![allow(clippy::float_cmp)] // Generated code
#![allow(clippy::no_effect_underscore_binding)] // Generated code
#![allow(clippy::uninlined_format_args)] // Generated code
#![allow(clippy::needless_pass_by_value)] // Generated code
#![allow(clippy::redundant_closure_for_method_calls)] // Generated code
#![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 crossbeam_channel::{unbounded, Receiver, Sender};
// use std::rc::Rc;
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_progress_receiver::connect_progress_gathering;
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};
slint::include_modules!();
fn main() {
setup_logger(false);
print_version_mode();
let app = MainWindow::new().unwrap();
let (progress_sender, progress_receiver): (Sender<ProgressData>, Receiver<ProgressData>) = unbounded();
let (stop_sender, stop_receiver): (Sender<()>, Receiver<()>) = unbounded();
// to_remove_debug(&app);
set_initial_gui_infos(&app);
create_default_settings_files();
load_settings_from_file(&app);
connect_delete_button(&app);
connect_scan_button(&app, progress_sender, stop_receiver);
connect_stop_button(&app, stop_sender);
connect_open_items(&app);
connect_progress_gathering(&app, progress_receiver);
connect_add_remove_directories(&app);
connect_show_preview(&app);
connect_translations(&app);
connect_changing_settings_preset(&app);
app.run().unwrap();
save_all_settings_to_file(&app);
}
// // TODO remove this after debugging - or leave commented
// pub fn to_remove_debug(app: &MainWindow) {
// app.set_empty_folder_model(to_remove_create_without_header("@@").into());
// app.set_empty_files_model(to_remove_create_without_header("%%").into());
// app.set_similar_images_model(to_remove_create_with_header().into());
// }
// fn to_remove_create_with_header() -> Rc<VecModel<MainListModel>> {
// let header_row_data: Rc<VecModel<MainListModel>> = Rc::new(VecModel::default());
// for r in 0..10_000 {
// let items = VecModel::default();
//
// for c in 0..3 {
// items.push(slint::format!("Item {r}.{c}"));
// }
//
// let is_header = r % 3 == 0;
// let is_checked = (r % 2 == 0) && !is_header;
//
// let item = MainListModel {
// checked: is_checked,
// header_row: is_header,
// selected_row: false,
// val: ModelRc::new(items),
// };
//
// header_row_data.push(item);
// }
// header_row_data
// }
// fn to_remove_create_without_header(s: &str) -> Rc<VecModel<MainListModel>> {
// let non_header_row_data: Rc<VecModel<MainListModel>> = Rc::new(VecModel::default());
// for r in 0..100_000 {
// let items = VecModel::default();
//
// for c in 0..3 {
// items.push(slint::format!("Item {r}.{c}.{s}"));
// }
//
// let is_checked = r % 2 == 0;
//
// let item = MainListModel {
// checked: is_checked,
// header_row: false,
// selected_row: false,
// val: ModelRc::new(items),
// };
//
// non_header_row_data.push(item);
// }
// non_header_row_data
// }

View file

@ -0,0 +1,32 @@
use czkawka_core::common::get_available_threads;
use slint::{ComponentHandle, SharedString, VecModel};
use crate::settings::{ALLOWED_HASH_SIZE_VALUES, ALLOWED_HASH_TYPE_VALUES, ALLOWED_RESIZE_ALGORITHM_VALUES};
use crate::GuiState;
use crate::MainWindow;
use crate::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 settings = app.global::<Settings>();
app.global::<GuiState>().set_maximum_threads(threads as f32);
let available_hash_size: Vec<SharedString> = ALLOWED_HASH_SIZE_VALUES
.iter()
.map(|(hash_size, _max_similarity)| hash_size.to_string().into())
.collect::<Vec<_>>();
let available_resize_algorithm: Vec<SharedString> = ALLOWED_RESIZE_ALGORITHM_VALUES
.iter()
.map(|(_settings_key, gui_name, _filter_type)| (*gui_name).into())
.collect::<Vec<_>>();
let available_hash_type: Vec<SharedString> = ALLOWED_HASH_TYPE_VALUES
.iter()
.map(|(_settings_key, gui_name, _hash_type)| (*gui_name).into())
.collect::<Vec<_>>();
settings.set_similar_images_sub_available_hash_size(VecModel::from_slice(&available_hash_size));
settings.set_similar_images_sub_available_resize_algorithm(VecModel::from_slice(&available_resize_algorithm));
settings.set_similar_images_sub_available_hash_type(VecModel::from_slice(&available_hash_type));
}

578
krokiet/src/settings.rs Normal file
View file

@ -0,0 +1,578 @@
use std::cmp::{max, min};
use std::env;
use std::path::PathBuf;
use directories_next::ProjectDirs;
use home::home_dir;
use image_hasher::{FilterType, HashAlg};
use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use slint::{ComponentHandle, Model, ModelRc};
use czkawka_core::common::{get_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::{Callabler, MainWindow};
use crate::{GuiState, Settings};
pub const DEFAULT_MINIMUM_SIZE_KB: i32 = 16;
pub const DEFAULT_MAXIMUM_SIZE_KB: i32 = i32::MAX / 1024;
pub const DEFAULT_MINIMUM_CACHE_SIZE: i32 = 256;
pub const DEFAULT_MINIMUM_PREHASH_CACHE_SIZE: i32 = 256;
// (Hash size, Maximum difference) - Ehh... to simplify it, just use everywhere 40 as maximum similarity - for now I'm to lazy to change it, when hash size changes
// So if you want to change it, you need to change it in multiple places
pub const ALLOWED_HASH_SIZE_VALUES: &[(u8, u8)] = &[(8, 40), (16, 40), (32, 40), (64, 40)];
pub const ALLOWED_RESIZE_ALGORITHM_VALUES: &[(&str, &str, FilterType)] = &[
("lanczos3", "Lanczos3", FilterType::Lanczos3),
("gaussian", "Gaussian", FilterType::Gaussian),
("catmullrom", "CatmullRom", FilterType::CatmullRom),
("triangle", "Triangle", FilterType::Triangle),
("nearest", "Nearest", FilterType::Nearest),
];
pub const ALLOWED_HASH_TYPE_VALUES: &[(&str, &str, HashAlg)] = &[
("mean", "Mean", HashAlg::Mean),
("gradient", "Gradient", HashAlg::Gradient),
("blockhash", "BlockHash", HashAlg::Blockhash),
("vertgradient", "VertGradient", HashAlg::VertGradient),
("doublegradient", "DoubleGradient", HashAlg::DoubleGradient),
];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsCustom {
#[serde(default = "default_included_directories")]
pub included_directories: 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 = "minimum_file_size")]
pub minimum_file_size: i32,
#[serde(default = "maximum_file_size")]
pub maximum_file_size: i32,
#[serde(default = "ttrue")]
pub recursive_search: bool,
#[serde(default = "ttrue")]
pub use_cache: bool,
#[serde(default)]
pub save_also_as_json: bool,
#[serde(default)]
pub move_deleted_files_to_trash: bool,
#[serde(default)]
pub ignore_other_file_systems: bool,
#[serde(default)]
pub thread_number: i32,
#[serde(default = "ttrue")]
pub duplicate_image_preview: bool,
#[serde(default = "ttrue")]
pub duplicate_hide_hard_links: bool,
#[serde(default = "ttrue")]
pub duplicate_use_prehash: bool,
#[serde(default = "minimal_hash_cache_size")]
pub duplicate_minimal_hash_cache_size: i32,
#[serde(default = "minimal_prehash_cache_size")]
pub duplicate_minimal_prehash_cache_size: i32,
#[serde(default = "ttrue")]
pub duplicate_delete_outdated_entries: bool,
#[serde(default = "ttrue")]
pub similar_images_show_image_preview: bool,
#[serde(default = "ttrue")]
pub similar_images_delete_outdated_entries: bool,
#[serde(default = "ttrue")]
pub similar_videos_delete_outdated_entries: bool,
#[serde(default = "ttrue")]
pub similar_music_delete_outdated_entries: bool,
#[serde(default = "default_sub_hash_size")]
pub similar_images_sub_hash_size: u8,
#[serde(default = "default_hash_type")]
pub similar_images_sub_hash_type: String,
#[serde(default = "default_resize_algorithm")]
pub similar_images_sub_resize_algorithm: String,
#[serde(default)]
pub similar_images_sub_ignore_same_size: bool,
#[serde(default = "default_similarity")]
pub similar_images_sub_similarity: i32,
}
pub fn default_similarity() -> i32 {
10
}
impl Default for SettingsCustom {
fn default() -> Self {
serde_json::from_str("{}").unwrap()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BasicSettings {
#[serde(default = "default_language")]
pub language: String,
#[serde(default)]
pub default_preset: i32,
#[serde(default = "default_preset_names")]
pub preset_names: Vec<String>,
}
impl Default for BasicSettings {
fn default() -> Self {
serde_json::from_str("{}").unwrap()
}
}
pub fn connect_changing_settings_preset(app: &MainWindow) {
let a = app.as_weak();
app.global::<Callabler>().on_changed_settings_preset(move || {
let app = a.upgrade().unwrap();
let current_item = app.global::<Settings>().get_settings_preset_idx();
let loaded_data = load_data_from_file::<SettingsCustom>(get_config_file(current_item));
match loaded_data {
Ok(loaded_data) => {
set_settings_to_gui(&app, &loaded_data);
app.set_text_summary_text(format!("Changed and loaded properly preset {}", current_item + 1).into());
}
Err(e) => {
set_settings_to_gui(&app, &SettingsCustom::default());
app.set_text_summary_text(format!("Cannot change and load preset {} - reason {e}", current_item + 1).into());
}
}
});
let a = app.as_weak();
app.global::<Callabler>().on_save_current_preset(move || {
let app = a.upgrade().unwrap();
let settings = app.global::<Settings>();
let current_item = settings.get_settings_preset_idx();
let result = save_data_to_file(get_config_file(current_item), &collect_settings(&app));
match result {
Ok(()) => {
app.set_text_summary_text(format!("Saved preset {}", current_item + 1).into());
}
Err(e) => {
app.set_text_summary_text(format!("Cannot save preset {} - reason {e}", current_item + 1).into());
error!("{e}");
}
}
});
let a = app.as_weak();
app.global::<Callabler>().on_reset_current_preset(move || {
let app = a.upgrade().unwrap();
let settings = app.global::<Settings>();
let current_item = settings.get_settings_preset_idx();
set_settings_to_gui(&app, &SettingsCustom::default());
app.set_text_summary_text(format!("Reset preset {}", current_item + 1).into());
});
let a = app.as_weak();
app.global::<Callabler>().on_load_current_preset(move || {
let app = a.upgrade().unwrap();
let settings = app.global::<Settings>();
let current_item = settings.get_settings_preset_idx();
let loaded_data = load_data_from_file::<SettingsCustom>(get_config_file(current_item));
match loaded_data {
Ok(loaded_data) => {
set_settings_to_gui(&app, &loaded_data);
app.set_text_summary_text(format!("Loaded preset {}", current_item + 1).into());
}
Err(e) => {
set_settings_to_gui(&app, &SettingsCustom::default());
let err_message = format!("Cannot load preset {} - reason {e}", current_item + 1);
app.set_text_summary_text(err_message.into());
error!("{e}");
}
}
});
}
pub fn create_default_settings_files() {
let base_config_file = get_base_config_file();
if let Some(base_config_file) = base_config_file {
if !base_config_file.is_file() {
let _ = save_data_to_file(Some(base_config_file), &BasicSettings::default());
}
}
for i in 1..=10 {
let config_file = get_config_file(i);
if let Some(config_file) = config_file {
if !config_file.is_file() {
let _ = save_data_to_file(Some(config_file), &SettingsCustom::default());
}
}
}
}
pub fn load_settings_from_file(app: &MainWindow) {
let result_base_settings = load_data_from_file::<BasicSettings>(get_base_config_file());
let mut base_settings;
if let Ok(base_settings_temp) = result_base_settings {
base_settings = base_settings_temp;
} else {
info!("Cannot load base settings, using default instead");
base_settings = BasicSettings::default();
}
let results_custom_settings = load_data_from_file::<SettingsCustom>(get_config_file(base_settings.default_preset));
let mut custom_settings;
if let Ok(custom_settings_temp) = results_custom_settings {
custom_settings = custom_settings_temp;
} else {
info!("Cannot load custom settings, using default instead");
custom_settings = SettingsCustom::default();
}
// Validate here values and set "proper"
// preset_names should have 10 items
if base_settings.preset_names.len() > 10 {
base_settings.preset_names.truncate(10);
} else if base_settings.preset_names.len() < 10 {
while base_settings.preset_names.len() < 10 {
base_settings.preset_names.push(format!("Preset {}", base_settings.preset_names.len() + 1));
}
}
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);
// Ended validating
set_settings_to_gui(app, &custom_settings);
set_base_settings_to_gui(app, &base_settings);
set_number_of_threads(custom_settings.thread_number as usize);
}
pub fn save_all_settings_to_file(app: &MainWindow) {
save_base_settings_to_file(app);
save_custom_settings_to_file(app);
}
pub fn save_base_settings_to_file(app: &MainWindow) {
let result = save_data_to_file(get_base_config_file(), &collect_base_settings(app));
if let Err(e) = result {
error!("{e}");
}
}
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), &collect_settings(app));
if let Err(e) = result {
error!("{e}");
}
}
pub fn load_data_from_file<T>(config_file: Option<PathBuf>) -> Result<T, String>
where
for<'de> T: Deserialize<'de>,
{
let current_time = std::time::Instant::now();
let Some(config_file) = config_file else {
return Err("Cannot get config file".into());
};
if !config_file.is_file() {
return Err("Config file doesn't exists".into());
}
let result = match std::fs::read_to_string(&config_file) {
Ok(serialized) => match serde_json::from_str(&serialized) {
Ok(custom_settings) => Ok(custom_settings),
Err(e) => Err(format!("Cannot deserialize settings: {e}")),
},
Err(e) => Err(format!("Cannot read config file: {e}")),
};
debug!("Loading data from file {:?} took {:?}", config_file, current_time.elapsed());
result
}
pub fn save_data_to_file<T>(config_file: Option<PathBuf>, serializable_data: &T) -> Result<(), String>
where
T: Serialize,
{
let current_time = std::time::Instant::now();
let Some(config_file) = config_file else {
return Err("Cannot get config file".into());
};
// Create dirs if not exists
if let Some(parent) = config_file.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
return Err(format!("Cannot create config folder: {e}"));
}
}
match serde_json::to_string_pretty(&serializable_data) {
Ok(serialized) => {
if let Err(e) = std::fs::write(&config_file, serialized) {
return Err(format!("Cannot save config file: {e}"));
}
}
Err(e) => {
return Err(format!("Cannot serialize settings: {e}"));
}
}
debug!("Saving data to file {:?} took {:?}", config_file, current_time.elapsed());
Ok(())
}
pub fn get_base_config_file() -> Option<PathBuf> {
let configs = ProjectDirs::from("pl", "Qarmin", "Krokiet")?;
let config_folder = configs.config_dir();
let base_config_file = config_folder.join("config_general.json");
Some(base_config_file)
}
pub fn get_config_file(number: i32) -> Option<PathBuf> {
let configs = ProjectDirs::from("pl", "Qarmin", "Krokiet")?;
let config_folder = configs.config_dir();
let config_file = config_folder.join(format!("config_preset_{number}.json"));
Some(config_file)
}
pub fn set_base_settings_to_gui(app: &MainWindow, basic_settings: &BasicSettings) {
let settings = app.global::<Settings>();
// settings.set_language(basic_settings.language.clone());
settings.set_settings_preset_idx(basic_settings.default_preset);
settings.set_settings_presets(ModelRc::new(create_vec_model_from_vec_string(basic_settings.preset_names.clone())));
}
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);
// Excluded directories
let excluded_directories = create_string_standard_list_view_from_pathbuf(&custom_settings.excluded_directories);
settings.set_excluded_directories(excluded_directories);
settings.set_excluded_items(custom_settings.excluded_items.clone().into());
settings.set_allowed_extensions(custom_settings.allowed_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);
settings.set_save_as_json(custom_settings.save_also_as_json);
settings.set_move_to_trash(custom_settings.move_deleted_files_to_trash);
settings.set_ignore_other_filesystems(custom_settings.ignore_other_file_systems);
settings.set_thread_number(custom_settings.thread_number as f32);
settings.set_recursive_search(custom_settings.recursive_search);
settings.set_duplicate_image_preview(custom_settings.duplicate_image_preview);
settings.set_duplicate_hide_hard_links(custom_settings.duplicate_hide_hard_links);
settings.set_duplicate_use_prehash(custom_settings.duplicate_use_prehash);
settings.set_duplicate_minimal_hash_cache_size(custom_settings.duplicate_minimal_hash_cache_size.to_string().into());
settings.set_duplicate_minimal_prehash_cache_size(custom_settings.duplicate_minimal_prehash_cache_size.to_string().into());
settings.set_duplicate_delete_outdated_entries(custom_settings.duplicate_delete_outdated_entries);
settings.set_similar_images_show_image_preview(custom_settings.similar_images_show_image_preview);
settings.set_similar_images_delete_outdated_entries(custom_settings.similar_images_delete_outdated_entries);
settings.set_similar_videos_delete_outdated_entries(custom_settings.similar_videos_delete_outdated_entries);
settings.set_similar_music_delete_outdated_entries(custom_settings.similar_music_delete_outdated_entries);
let similar_images_sub_hash_size_idx = if let Some(idx) = ALLOWED_HASH_SIZE_VALUES
.iter()
.position(|(hash_size, _max_similarity)| *hash_size == custom_settings.similar_images_sub_hash_size)
{
idx
} else {
warn!(
"Value of hash size \"{}\" is invalid, setting it to default value",
custom_settings.similar_images_sub_hash_size
);
0
};
settings.set_similar_images_sub_hash_size_index(similar_images_sub_hash_size_idx as i32);
let similar_images_sub_hash_type_idx = if let Some(idx) = ALLOWED_HASH_TYPE_VALUES
.iter()
.position(|(settings_key, _gui_name, _hash_type)| *settings_key == custom_settings.similar_images_sub_hash_type)
{
idx
} else {
warn!(
"Value of hash type \"{}\" is invalid, setting it to default value",
custom_settings.similar_images_sub_hash_type
);
0
};
settings.set_similar_images_sub_hash_type_index(similar_images_sub_hash_type_idx as i32);
let similar_images_sub_resize_algorithm_idx = if let Some(idx) = ALLOWED_RESIZE_ALGORITHM_VALUES
.iter()
.position(|(settings_key, _gui_name, _resize_alg)| *settings_key == custom_settings.similar_images_sub_resize_algorithm)
{
idx
} else {
warn!(
"Value of resize algorithm \"{}\" is invalid, setting it to default value",
custom_settings.similar_images_sub_resize_algorithm
);
0
};
settings.set_similar_images_sub_resize_algorithm_index(similar_images_sub_resize_algorithm_idx as i32);
settings.set_similar_images_sub_ignore_same_size(custom_settings.similar_images_sub_ignore_same_size);
settings.set_similar_images_sub_max_similarity(40.0); // TODO this is now set to stable 40
settings.set_similar_images_sub_current_similarity(custom_settings.similar_images_sub_similarity as f32);
// Clear text
app.global::<GuiState>().set_info_text("".into());
}
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 excluded_directories = settings.get_excluded_directories();
let excluded_directories = excluded_directories.iter().map(|x| PathBuf::from(x.text.as_str())).collect::<Vec<_>>();
let excluded_items = settings.get_excluded_items().to_string();
let allowed_extensions = settings.get_allowed_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);
let recursive_search = settings.get_recursive_search();
let use_cache = settings.get_use_cache();
let save_also_as_json = settings.get_save_as_json();
let move_deleted_files_to_trash = settings.get_move_to_trash();
let ignore_other_file_systems = settings.get_ignore_other_filesystems();
let thread_number = settings.get_thread_number().round() as i32;
let duplicate_image_preview = settings.get_duplicate_image_preview();
let duplicate_hide_hard_links = settings.get_duplicate_hide_hard_links();
let duplicate_use_prehash = settings.get_duplicate_use_prehash();
let duplicate_minimal_hash_cache_size = settings.get_duplicate_minimal_hash_cache_size().parse::<i32>().unwrap_or(DEFAULT_MINIMUM_CACHE_SIZE);
let duplicate_minimal_prehash_cache_size = settings
.get_duplicate_minimal_prehash_cache_size()
.parse::<i32>()
.unwrap_or(DEFAULT_MINIMUM_PREHASH_CACHE_SIZE);
let duplicate_delete_outdated_entries = settings.get_duplicate_delete_outdated_entries();
let similar_images_show_image_preview = settings.get_similar_images_show_image_preview();
let similar_images_delete_outdated_entries = settings.get_similar_images_delete_outdated_entries();
let similar_videos_delete_outdated_entries = settings.get_similar_videos_delete_outdated_entries();
let similar_music_delete_outdated_entries = settings.get_similar_music_delete_outdated_entries();
let similar_images_sub_hash_size_idx = settings.get_similar_images_sub_hash_size_index();
let similar_images_sub_hash_size = ALLOWED_HASH_SIZE_VALUES[similar_images_sub_hash_size_idx as usize].0;
let similar_images_sub_hash_type_idx = settings.get_similar_images_sub_hash_type_index();
let similar_images_sub_hash_type = ALLOWED_HASH_TYPE_VALUES[similar_images_sub_hash_type_idx as usize].0.to_string();
let similar_images_sub_resize_algorithm_idx = settings.get_similar_images_sub_resize_algorithm_index();
let similar_images_sub_resize_algorithm = ALLOWED_RESIZE_ALGORITHM_VALUES[similar_images_sub_resize_algorithm_idx as usize].0.to_string();
let similar_images_sub_ignore_same_size = settings.get_similar_images_sub_ignore_same_size();
let similar_images_sub_similarity = settings.get_similar_images_sub_current_similarity().round() as i32;
SettingsCustom {
included_directories,
excluded_directories,
excluded_items,
allowed_extensions,
minimum_file_size,
maximum_file_size,
recursive_search,
use_cache,
save_also_as_json,
move_deleted_files_to_trash,
ignore_other_file_systems,
thread_number,
duplicate_image_preview,
duplicate_hide_hard_links,
duplicate_use_prehash,
duplicate_minimal_hash_cache_size,
duplicate_minimal_prehash_cache_size,
duplicate_delete_outdated_entries,
similar_images_show_image_preview,
similar_images_delete_outdated_entries,
similar_videos_delete_outdated_entries,
similar_music_delete_outdated_entries,
similar_images_sub_hash_size,
similar_images_sub_hash_type,
similar_images_sub_resize_algorithm,
similar_images_sub_ignore_same_size,
similar_images_sub_similarity,
}
}
pub fn collect_base_settings(app: &MainWindow) -> BasicSettings {
let settings = app.global::<Settings>();
let default_preset = settings.get_settings_preset_idx();
let preset_names = settings.get_settings_presets().iter().map(|x| x.to_string()).collect::<Vec<_>>();
assert_eq!(preset_names.len(), 10);
BasicSettings {
language: "en".to_string(),
default_preset,
preset_names,
}
}
fn default_included_directories() -> Vec<PathBuf> {
let mut included_directories = vec![];
if let Ok(current_dir) = env::current_dir() {
included_directories.push(current_dir.to_string_lossy().to_string());
} else if let Some(home_dir) = home_dir() {
included_directories.push(home_dir.to_string_lossy().to_string());
} else if cfg!(target_family = "unix") {
included_directories.push("/".to_string());
} else {
// This could be set to default
included_directories.push("C:\\".to_string());
};
included_directories.sort();
included_directories.iter().map(PathBuf::from).collect::<Vec<_>>()
}
fn default_excluded_directories() -> Vec<PathBuf> {
let mut excluded_directories = DEFAULT_EXCLUDED_DIRECTORIES.iter().map(PathBuf::from).collect::<Vec<_>>();
excluded_directories.sort();
excluded_directories
}
fn default_excluded_items() -> String {
DEFAULT_EXCLUDED_ITEMS.to_string()
}
fn default_language() -> String {
"en".to_string()
}
fn default_preset_names() -> Vec<String> {
(0..10).map(|x| format!("Preset {}", x + 1)).collect::<Vec<_>>()
}
fn minimum_file_size() -> i32 {
DEFAULT_MINIMUM_SIZE_KB
}
fn maximum_file_size() -> i32 {
DEFAULT_MAXIMUM_SIZE_KB
}
fn ttrue() -> bool {
true
}
fn minimal_hash_cache_size() -> i32 {
DEFAULT_MINIMUM_CACHE_SIZE
}
fn minimal_prehash_cache_size() -> i32 {
DEFAULT_MINIMUM_PREHASH_CACHE_SIZE
}
pub fn default_resize_algorithm() -> String {
ALLOWED_RESIZE_ALGORITHM_VALUES[0].0.to_string()
}
pub fn default_hash_type() -> String {
ALLOWED_HASH_TYPE_VALUES[0].0.to_string()
}
pub fn default_sub_hash_size() -> u8 {
16
}

View file

@ -0,0 +1,120 @@
import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox} from "std-widgets.slint";
import {LeftSidePanel} from "left_side_panel.slint";
import {MainList} from "main_lists.slint";
import {CurrentTab} from "common.slint";
import {BottomPanelVisibility} from "common.slint";
import {Callabler} from "callabler.slint";
import {GuiState} from "gui_state.slint";
export component VisibilityButton inherits Button {
in-out property <BottomPanelVisibility> button_visibility;
in-out property <BottomPanelVisibility> bottom_panel_visibility;
enabled: bottom_panel_visibility != button-visibility;
height: 30px;
width: 70px;
clicked => {
bottom-panel-visibility = button_visibility;
}
}
export component ActionButtons inherits HorizontalLayout {
callback scan_stopping;
callback scan_starting(CurrentTab);
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;
Rectangle {
scan_button := Button {
height: parent.height;
enabled: !scanning && lists_enabled;
visible: !scanning;
text: "Scan";
clicked => {
root.scanning = true;
root.scan_starting(GuiState.active_tab);
}
}
stop_button := Button {
height: parent.height;
visible: scanning;
enabled: scanning && !stop_requested && root.lists_enabled;
text: "Stop";
clicked => {
root.scan_stopping();
root.stop_requested = true;
}
}
}
Rectangle {
horizontal-stretch: 0.5;
}
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();
}
}
Rectangle {
horizontal-stretch: 0.5;
}
HorizontalLayout {
padding: 0px;
spacing: 0px;
VisibilityButton {
height: parent.height;
button-visibility: BottomPanelVisibility.Directories;
bottom_panel_visibility <=> bottom_panel_visibility;
text: "Dirs";
}
VisibilityButton {
height: parent.height;
button-visibility: BottomPanelVisibility.TextErrors;
bottom_panel_visibility <=> bottom_panel_visibility;
text: "Text";
}
VisibilityButton {
height: parent.height;
button-visibility: BottomPanelVisibility.NotVisible;
bottom_panel_visibility <=> bottom_panel_visibility;
text: "None";
}
}
}

View file

@ -0,0 +1,126 @@
import {Button, StandardListView, VerticalBox, ScrollView, TextEdit} from "std-widgets.slint";
import {Settings} from "settings.slint";
import {BottomPanelVisibility} from "common.slint";
import {Callabler} from "callabler.slint";
import {GuiState} from "gui_state.slint";
component DirectoriesPanel inherits HorizontalLayout {
callback folder_choose_requested(bool);
callback show_manual_add_dialog(bool);
// Included directories
VerticalLayout {
horizontal-stretch: 0.0;
spacing: 5px;
Button {
text: "Add";
clicked => {
folder_choose_requested(true);
}
}
Button {
text: "Remove";
clicked => {
Callabler.remove_item_directories(true, included-list.current-item);
}
}
Button {
text: "Manual Add";
clicked => {
show_manual_add_dialog(true);
}
}
Rectangle {
vertical-stretch: 1.0;
}
}
VerticalLayout {
horizontal-stretch: 1.0;
Rectangle {
Text {
text: "Included Directories";
}
}
included_list := StandardListView {
model: Settings.included-directories;
}
}
// Excluded directories
VerticalLayout {
horizontal-stretch: 0.0;
spacing: 5px;
Button {
text: "Add";
clicked => {
folder_choose_requested(false);
}
}
Button {
text: "Remove";
clicked => {
Callabler.remove_item_directories(false, excluded-list.current-item);
}
}
Button {
text: "Manual Add";
clicked => {
show_manual_add_dialog(false);
}
}
Rectangle {
vertical-stretch: 1.0;
}
}
VerticalLayout {
horizontal-stretch: 1.0;
Rectangle {
Text {
text: "Excluded Directories";
}
}
excluded_list := StandardListView {
model: Settings.excluded-directories;
}
}
}
component TextErrorsPanel inherits TextEdit {
height: 20px;
read-only: true;
wrap: TextWrap.no-wrap;
text <=> GuiState.info_text;
}
export component BottomPanel {
in-out property <BottomPanelVisibility> bottom_panel_visibility: BottomPanelVisibility.Directories;
callback folder_choose_requested(bool);
callback show_manual_add_dialog(bool);
min-height: bottom-panel-visibility == BottomPanelVisibility.NotVisible ? 0px : 150px;
min-width: bottom-panel-visibility == BottomPanelVisibility.NotVisible ? 0px : 400px;
if bottom-panel-visibility == BottomPanelVisibility.Directories: DirectoriesPanel {
width: parent.width;
height: parent.height;
folder_choose_requested(included-directories) => {
root.folder_choose_requested(included-directories)
}
show_manual_add_dialog(included-directories) => {
root.show_manual_add_dialog(included-directories)
}
}
if bottom-panel-visibility == BottomPanelVisibility.TextErrors: TextErrorsPanel {
width: parent.width;
height: parent.height;
}
}

View file

@ -0,0 +1,29 @@
export global Callabler {
// Bottom panel operations
callback remove_item_directories(bool, int);
callback added_manual_directories(bool, string);
// Right click or middle click opener
callback item_opened(string);
callback delete_selected_items();
// callback ();
// Preview
callback load_image_preview(string);
// Settings
callback changed_settings_preset();
callback save_current_preset();
callback load_current_preset();
callback reset_current_preset();
// Translations
pure callback translate(string, [{key: string, value: string}]) -> string;
// Only Slint
callback open_select_popup();
callback open_config_folder();
callback open_cache_folder();
}

View file

@ -0,0 +1,13 @@
import { StyleMetrics } from "std-widgets.slint";
export global ColorPalette {
// Tabs at left side
in-out property <color> tab_selected_color: StyleMetrics.dark-color-scheme ? #353535 : #5e5e5e;
in-out property <color> tab_hovered_color: StyleMetrics.dark-color-scheme ? #49494926 : #80808014;
// ListView
in-out property <color> list_view_normal_color: StyleMetrics.dark-color-scheme ? #222222 : #dddddd;
in-out property <color> list_view_normal_header_color: StyleMetrics.dark-color-scheme ? #111111 : #888888;
in-out property <color> list_view_normal_selected_header: StyleMetrics.dark-color-scheme ? #444444 : #cccccc;
// Popup
in-out property <color> popup_background: StyleMetrics.dark-color-scheme ? #353535 : #5e5e5e;
}

30
krokiet/ui/common.slint Normal file
View file

@ -0,0 +1,30 @@
export enum CurrentTab {
EmptyFolders,
EmptyFiles,
SimilarImages,
Settings
}
export enum TypeOfOpenedItem {
CurrentItem,
ParentItem,
}
export struct ProgressToSend {
current_progress: int,
all_progress: int,
step_name: string,
}
export struct MainListModel {
checked: bool,
header_row: bool,
selected_row: bool,
val: [string]
}
export enum BottomPanelVisibility {
NotVisible,
TextErrors,
Directories
}

View file

@ -0,0 +1,19 @@
import {CurrentTab} from "common.slint";
// State to show
export global GuiState {
in-out property <length> app_width;
in-out property <length> app_height;
in-out property <string> info_text: "Nothing to report";
in-out property <bool> preview_visible;
in-out property <image> preview_image;
in-out property <float> maximum_threads: 40;
in-out property <bool> choosing_include_directories;
in-out property <bool> visible_tool_settings;
in-out property <bool> available_subsettings: active_tab == CurrentTab.SimilarImages;
in-out property <CurrentTab> active_tab: CurrentTab.EmptyFiles;
}

View file

@ -0,0 +1,139 @@
import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox} from "std-widgets.slint";
import {CurrentTab} from "common.slint";
import {ColorPalette} from "color_palette.slint";
import {GuiState} from "gui_state.slint";
component TabItem {
in property <bool> scanning;
in property <string> text;
in property <CurrentTab> curr_tab;
callback changed_current_tab();
Rectangle {
width: parent.width;
horizontal-stretch: 1.0;
background: touch-area.has-hover ? ColorPalette.tab-hovered-color : transparent;
touch_area := TouchArea {
clicked => {
if (GuiState.active_tab == root.curr-tab) {
return;
}
GuiState.active_tab = root.curr-tab;
changed_current_tab();
}
}
}
HorizontalLayout {
width: parent.width;
alignment: LayoutAlignment.end;
layout_rectangle := VerticalLayout {
empty_rectangle := Rectangle { }
current_rectangle := Rectangle {
visible: (GuiState.active_tab == root.curr-tab);
border-radius: 2px;
width: 5px;
height: 0px;
background: ColorPalette.tab_selected_color;
animate height{
duration: 150ms;
easing: ease;
}
}
empty_rectangle2 := Rectangle { }
}
}
Text {
text: root.text;
width: parent.width;
horizontal-alignment: center;
}
states [
is-selected when GuiState.active_tab == root.curr-tab: {
current_rectangle.height: layout_rectangle.height;
}
is-not-selected when GuiState.active_tab != root.curr-tab: {
current_rectangle.height: 0px;
}
]
}
export component LeftSidePanel {
in-out property <bool> scanning;
callback changed_current_tab();
width: 120px;
VerticalLayout {
spacing: 20px;
Rectangle {
height: 100px;
Image {
width: root.width;
source: @image-url("../icons/logo.png");
}
}
VerticalLayout {
// spacing: 3px;
alignment: center;
out property <length> element-size: 25px;
TabItem {
height: parent.element-size;
scanning: scanning;
text: "Empty Folders";
curr_tab: CurrentTab.EmptyFolders;
changed_current_tab() => {root.changed_current_tab();}
}
TabItem {
height: parent.element-size;
scanning: scanning;
text: "Empty Files";
curr_tab: CurrentTab.EmptyFiles;
changed_current_tab() => {root.changed_current_tab();}
}
TabItem {
height: parent.element-size;
scanning: scanning;
text: "Similar Images";
curr_tab: CurrentTab.SimilarImages;
changed_current_tab() => {root.changed_current_tab();}
}
}
Rectangle {
HorizontalLayout {
alignment: start;
Button {
enabled: GuiState.active_tab != CurrentTab.Settings && GuiState.available_subsettings;
min-width: 20px;
min-height: 20px;
max-height: self.width;
preferred-height: self.width;
icon: @image-url("../icons/settings.svg");
clicked => {
GuiState.visible_tool_settings = !GuiState.visible-tool-settings;
}
}
}
HorizontalLayout {
alignment: end;
Button {
enabled: GuiState.active_tab != CurrentTab.Settings;
min-width: 20px;
min-height: 20px;
max-height: self.width;
preferred-height: self.width;
icon: @image-url("../icons/settings.svg");
clicked => {
GuiState.active_tab = CurrentTab.Settings;
root.changed_current_tab();
}
}
}
}
}
}

View file

@ -0,0 +1,83 @@
import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox} from "std-widgets.slint";
import {SelectableTableView} from "selectable_tree_view.slint";
import {LeftSidePanel} from "left_side_panel.slint";
import {CurrentTab, TypeOfOpenedItem} from "common.slint";
import {MainListModel} from "common.slint";
import {SettingsList} from "settings_list.slint";
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"]}
];
in-out property <[MainListModel]> empty_files_model;
in-out property <[MainListModel]> similar_images_model;
callback changed_current_tab();
callback released_key(string);
empty_folders := SelectableTableView {
visible: GuiState.active_tab == CurrentTab.EmptyFolders;
min-width: 200px;
height: parent.height;
columns: ["Selection", "Folder Name", "Path", "Modification Date"];
column-sizes: [35px, 100px, 350px, 150px];
values <=> empty-folder-model;
parentPathIdx: 2;
fileNameIdx: 1;
}
empty_files := SelectableTableView {
visible: GuiState.active_tab == CurrentTab.EmptyFiles;
min-width: 200px;
height: parent.height;
columns: ["Selection", "File Name", "Path", "Modification Date"];
column-sizes: [35px, 100px, 350px, 150px];
values <=> empty-files-model;
parentPathIdx: 2;
fileNameIdx: 1;
}
similar_images := SelectableTableView {
visible: GuiState.active_tab == CurrentTab.SimilarImages;
min-width: 200px;
height: parent.height;
columns: ["Selection", "Similarity", "Size", "Dimensions", "File Name", "Path", "Modification Date"];
column-sizes: [35px, 80px, 80px, 80px, 100px, 350px, 150px];
values <=> similar-images-model;
parentPathIdx: 5;
fileNameIdx: 4;
}
settings_list := SettingsList {
visible: GuiState.active_tab == CurrentTab.Settings;
}
focus_item := FocusScope {
width: 0px; // Hack to not steal first click from other components - https://github.com/slint-ui/slint/issues/3503
// Hack not works https://github.com/slint-ui/slint/issues/3503#issuecomment-1817809834 because disables key-released event
key-released(event) => {
if (!self.visible || !self.has-focus) {
return accept;
}
if (GuiState.active_tab == CurrentTab.EmptyFiles) {
empty_files.released_key(event);
} else if (GuiState.active_tab == CurrentTab.EmptyFolders) {
empty-folders.released_key(event);
} else if (GuiState.active_tab == CurrentTab.SimilarImages) {
similar-images.released_key(event);
} else {
debug("Non handled key in main_lists.slint");
}
accept
}
}
changed_current_tab() => {
empty_folders.deselect_selected_item();
empty_files.deselect_selected_item();
similar_images.deselect_selected_item();
}
}

View file

@ -0,0 +1,156 @@
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";
import {PopupNewDirectories} from "popup_new_directories.slint";
import { PopupSelect } from "popup_select.slint";
import { ToolSettings } from "tool_settings.slint";
export {Settings, Callabler, GuiState}
export component MainWindow inherits Window {
callback scan_stopping;
callback scan_starting(CurrentTab);
callback folder_choose_requested(bool);
callback scan_ended(string);
min-width: 300px;
preferred-width: 800px;
min-height: 300px;
preferred-height: 600px;
in-out property <string> text_summary_text: "";
in-out property <bool> stop_requested: false;
in-out property <bool> scanning: false;
in-out property <ProgressToSend> progress_datas: {
current_progress: 15,
all_progress: 20,
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"]}
];
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"]}
];
in-out property <[MainListModel]> similar_images_model: [];
VerticalBox {
HorizontalBox {
vertical-stretch: 1.0;
preferred-height: 300px;
LeftSidePanel {
horizontal-stretch: 0.0;
scanning <=> root.scanning;
changed_current_tab() => {
GuiState.preview_visible = false;
main_list.changed_current_tab();
}
}
VerticalLayout {
horizontal-stretch: 1.0;
min_width: 300px;
Rectangle {
vertical-stretch: 1.0;
main_list := MainList {
x: 0;
width: preview_or_tool_settings.visible ? parent.width / 2 : parent.width;
height: parent.height;
horizontal-stretch: 0.5;
empty_folder_model <=> root.empty_folder_model;
empty_files_model <=> root.empty_files_model;
similar_images_model <=> root.similar_images_model;
}
preview_or_tool_settings := Rectangle {
visible: (GuiState.preview_visible || tool_settings.visible) && GuiState.active_tab != CurrentTab.Settings;
height: parent.height;
x: parent.width / 2;
width: self.visible ? parent.width / 2 : 0;
Preview {
height: parent.height;
width: parent.width;
visible: GuiState.preview_visible && !tool_settings.visible;
source: GuiState.preview_image;
image-fit: ImageFit.contain;
}
tool_settings := ToolSettings {
height: parent.height;
width: parent.width;
visible: GuiState.visible_tool_settings && GuiState.available_subsettings;
}
}
}
if root.scanning: Progress {
horizontal-stretch: 0.0;
progress_datas <=> root.progress_datas;
}
}
}
action_buttons := ActionButtons {
vertical-stretch: 0.0;
scanning <=> root.scanning;
stop_requested <=> root.stop-requested;
scan_stopping => {
text_summary_text = "Stopping scan, please wait...";
root.scan_stopping();
}
scan_starting(item) => {
text_summary_text = "Searching...";
root.scan_starting(item);
}
}
text_summary := LineEdit {
text: text_summary_text;
read-only: true;
}
bottom_panel := BottomPanel {
bottom-panel-visibility <=> action_buttons.bottom_panel_visibility;
vertical-stretch: 0.0;
folder_choose_requested(included_directories) => {
root.folder_choose_requested(included_directories)
}
show_manual_add_dialog(included_directories) => {
GuiState.choosing_include_directories = included_directories;
new_directory_popup_window.show_popup()
}
}
}
new_directory_popup_window := PopupNewDirectories {
height: root.height;
width: root.width;
}
// select_popup_window := PopupSelect {
// height: root.height;
// width: root.width;
// }
scan_ended(scan_text) => {
text_summary_text = scan_text;
root.scanning = false;
root.stop_requested = false;
}
}

View file

@ -0,0 +1,86 @@
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 PopupNewDirectories inherits Rectangle {
width: 400px;
height: 400px;
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);
debug("OK");
popup_window.close();
}
}
Button {
text: "Cancel";
clicked => {
debug("Cancel");
popup_window.close();
}
}
}
}
}
}
}
}
// Button {
// text:"KKK";
// clicked => {
// show-popup();
// }
// }
show_popup() => {
popup_window.show();
}
}

View file

@ -0,0 +1,74 @@
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();
}
}

3
krokiet/ui/preview.slint Normal file
View file

@ -0,0 +1,3 @@
export component Preview inherits Image {
}

68
krokiet/ui/progress.slint Normal file
View file

@ -0,0 +1,68 @@
import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox} 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 { ProgressIndicator } from "std-widgets.slint";
export component Progress {
in-out property <ProgressToSend> progress_datas;
preferred-width: 400px;
preferred-height: 40px;
VerticalLayout {
Text {
text: progress-datas.step-name;
horizontal-alignment: TextHorizontalAlignment.center;
}
HorizontalLayout {
spacing: 5px;
VerticalLayout {
spacing: 5px;
Text {
vertical-alignment: TextVerticalAlignment.center;
text: "Current Stage:";
}
Text {
vertical-alignment: TextVerticalAlignment.center;
text: "All Stages:";
}
}
VerticalLayout {
spacing: 5px;
VerticalLayout {
alignment: LayoutAlignment.center;
ProgressIndicator {
visible: progress_datas.current-progress >= -0.001;
height: 8px;
progress: progress_datas.current-progress / 100.0;
}
}
VerticalLayout {
alignment: LayoutAlignment.center;
ProgressIndicator {
height: 8px;
progress: progress_datas.all-progress / 100.0;
}
}
}
VerticalLayout {
spacing: 5px;
Text {
visible: progress_datas.current-progress >= -0.001;
vertical-alignment: TextVerticalAlignment.center;
text: progress_datas.current-progress + "%";
}
Text {
vertical-alignment: TextVerticalAlignment.center;
text: progress_datas.all-progress + "%";
}
}
}
}
}

View file

@ -0,0 +1,205 @@
import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, ScrollView} from "std-widgets.slint";
import {TypeOfOpenedItem} from "common.slint";
import {ColorPalette} from "color_palette.slint";
import {MainListModel} from "common.slint";
import {Callabler} from "callabler.slint";
import {GuiState} from "gui_state.slint";
export component SelectableTableView inherits Rectangle {
callback item_opened(string);
in property <[string]> columns;
in-out property <[MainListModel]> values: [
{checked: false, selected_row: false, header_row: true, val: ["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"]}
];
in-out property <[length]> column_sizes: [30px, 80px, 150px, 160px];
private property <int> column_number: column-sizes.length + 1;
// This idx, starts from zero, but since first is always a checkbox, and is not in model.val values, remove 1 from idx
in-out property <int> parentPathIdx;
in-out property <int> fileNameIdx;
in-out property <int> selected_item: -1;
out property <length> list_view_width: max(self.width - 20px, column_sizes[0] + column_sizes[1] + column_sizes[2] + column_sizes[3] + column_sizes[4] + column_sizes[5] + column_sizes[6] + column_sizes[7] + column_sizes[8] + column_sizes[9] + column_sizes[10] + column_sizes[11]);
VerticalBox {
padding: 0px;
ScrollView {
height: 30px;
viewport-x <=> list_view.viewport-x;
vertical-stretch: 0;
HorizontalLayout {
spacing: 5px;
for title [idx] in root.columns: HorizontalLayout {
width: root.column-sizes[idx];
Text {
overflow: elide;
text: title;
}
Rectangle {
width: 1px;
background: gray;
TouchArea {
width: 5px;
x: (parent.width - self.width) / 2;
property <length> cached;
pointer-event(event) => {
if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {
self.cached = root.column_sizes[idx];
}
}
moved => {
if (self.pressed) {
root.column_sizes[idx] += (self.mouse-x - self.pressed-x);
if (root.column_sizes[idx] < 20px) {
root.column_sizes[idx] = 20px;
}
}
}
mouse-cursor: ew-resize;
}
}
}
}
}
list_view := ListView {
padding: 0px;
min-width: 100px;
for r [idx] in root.values: Rectangle {
width: list_view_width;
border-radius: 5px;
height: 20px;
background: r.header-row ? ColorPalette.list_view_normal_header_color : (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 (!r.header_row) {
r.selected_row = !r.selected_row;
if (root.selected-item == -1) {
root.selected-item = idx;
} else {
if (r.selected_row == true) {
root.values[root.selected-item].selected_row = false;
root.selected-item = idx;
} else {
root.selected-item = -1;
}
}
if (root.selected_item != -1) {
Callabler.load_image_preview(r.val[root.parentPathIdx - 1] + "/" + r.val[root.fileNameIdx - 1]);
} else {
GuiState.preview_visible = false;
}
}
}
pointer-event(event) => {
// TODO this should be clicked by double-click
if (event.button == PointerEventButton.right && event.kind == PointerEventKind.up) {
Callabler.item_opened(r.val[root.parentPathIdx - 1])
} else if (event.button == PointerEventButton.middle && event.kind == PointerEventKind.up) {
Callabler.item_opened(r.val[root.parentPathIdx - 1] + "/" + r.val[root.fileNameIdx - 1])
}
}
}
HorizontalLayout {
CheckBox {
visible: !r.header-row;
checked: r.checked && !r.header-row;
width: root.column-sizes[0];
toggled => {
r.checked = self.checked;
}
}
HorizontalLayout {
spacing: 5px;
for f [idx] in r.val: Text {
width: root.column-sizes[idx + 1];
text: f;
font-size: 12px;
vertical-alignment: center;
overflow: elide;
}
}
}
}
}
}
public function deselect_selected_item() {
if (root.selected_item != -1) {
root.values[root.selected-item].selected_row = false;
root.selected-item = -1;
}
}
// TODO this should work with multiple selection and shift and control key - problably logic will need to be set in global state
public function released_key(event: KeyEvent) {
if (event.text == " ") {
if (root.selected_item != -1) {
root.values[root.selected_item].checked = !root.values[root.selected_item].checked;
}
} else if (event.text == Key.DownArrow) {
if (root.selected_item != -1) {
if (root.values.length - 1 == root.selected_item) {
// Last element, so unselect it
root.values[root.selected_item].selected_row = false;
root.selected_item = -1;
} else {
// Select next item, if next item is header row, then select second
// This should be safe, because header row should never be last item
root.values[root.selected_item].selected_row = false;
if (root.values[root.selected_item + 1].header_row) {
root.selected_item += 2;
} else {
root.selected_item += 1;
}
root.values[root.selected_item].selected_row = true;
}
} else {
// Select last item if nothing is selected
if (root.values.length > 0) {
if (root.values[0].header_row) {
root.selected_item = 1;
} else {
root.selected_item = 0;
}
root.values[root.selected_item].selected_row = true;
}
}
} else if (event.text == Key.UpArrow) {
if (root.selected_item != -1) {
if (root.selected_item == 0) {
// First element, so unselect it
root.values[root.selected_item].selected_row = false;
root.selected_item = -1;
} else {
root.values[root.selected_item].selected_row = false;
// Select previous item, if previous item is header row, then select second previous item
// This is safe, because if there is non header row upper, then can be easily selected,
// but otherwise is done -2 which for 1 (smallest possible item to set with header row) gives -1, so gives
// this non selected row
if (root.values[root.selected_item - 1].header_row) {
root.selected_item -= 2;
} else {
root.selected_item -= 1;
}
if (root.selected_item != -1) {
root.values[root.selected_item].selected_row = true;
}
}
} else {
// Select last item if nothing is selected
if (root.values.length > 0) {
root.selected_item = root.values.length - 1;
root.values[root.selected_item].selected_row = true;
}
}
}
}
}

47
krokiet/ui/settings.slint Normal file
View file

@ -0,0 +1,47 @@
export global Settings {
in-out property <int> settings_preset_idx: 0;
in-out property <[string]> settings_presets: ["Preset 1", "Preset 2"];
in-out property <[StandardListViewItem]> included_directories: [{text: "ABCD"}, {text: "BCDA"}];
in-out property <[StandardListViewItem]> excluded_directories: [{text: "ABCD"}, {text: "BCDA"}, {text: "CDFFF"}];
// Settings
in-out property <string> excluded_items: "Excluded items";
in-out property <string> allowed_extensions: "Allowed extensions";
in-out property <string> minimum_file_size: 0;
in-out property <string> maximum_file_size: 0;
in-out property <bool> recursive_search: true;
in-out property <bool> use_cache: false;
in-out property <bool> save_as_json: false;
in-out property <bool> move_to_trash: false;
in-out property <bool> ignore_other_filesystems: false;
in-out property <float> thread_number: 4;
in-out property <bool> duplicate_image_preview;
in-out property <bool> duplicate_hide_hard_links;
in-out property <bool> duplicate_use_prehash;
in-out property <string> duplicate_minimal_hash_cache_size;
in-out property <string> duplicate_minimal_prehash_cache_size;
in-out property <bool> duplicate_delete_outdated_entries;
in-out property <bool> similar_images_show_image_preview;
in-out property <bool> similar_images_delete_outdated_entries;
// in-out property <int> similar_videos_show_video_preview; // TODO - maybe someday
in-out property <bool> similar_videos_delete_outdated_entries;
in-out property <bool> similar_music_delete_outdated_entries;
// Allowed subsettings
// Duplicate
in-out property <[string]> similar_images_sub_available_hash_size: ["8", "16", "32", "64"];
in-out property <int> similar_images_sub_hash_size_index: 0;
in-out property <[string]> similar_images_sub_available_resize_algorithm: ["Lanczos3", "Nearest", "Triangle", "Gaussian", "CatmullRom"];
in-out property <int> similar_images_sub_resize_algorithm_index: 0;
in-out property <[string]> similar_images_sub_available_hash_type: ["Gradient", "Mean", "VertGradient", "BlockHash", "DoubleGradient"];
in-out property <int> similar_images_sub_hash_type_index: 0;
in-out property <float> similar_images_sub_max_similarity: 40;
in-out property <float> similar_images_sub_current_similarity: 20;
in-out property <bool> similar_images_sub_ignore_same_size;
}

View file

@ -0,0 +1,320 @@
import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, ScrollView, LineEdit, SpinBox, ComboBox, TextEdit, Slider} from "std-widgets.slint";
import { Settings } from "settings.slint";
import { Callabler } from "callabler.slint";
import { GuiState } from "gui_state.slint";
// TODO use Spinbox instead LineEdit {} to be able to set only numbers
global SettingsSize {
out property <length> item_height: 30px;
}
component TextComponent inherits HorizontalLayout {
in-out property <string> model;
in property <string> name;
spacing: 5px;
Text {
horizontal-stretch: 0.0;
vertical-alignment: TextVerticalAlignment.center;
text: name;
}
LineEdit {
horizontal-stretch: 1.0;
height: SettingsSize.item_height;
text <=> model;
}
}
component CheckBoxComponent inherits HorizontalLayout {
in-out property <bool> model;
in property <string> name;
spacing: 5px;
CheckBox {
horizontal-stretch: 1.0;
height: SettingsSize.item_height;
checked <=> model;
text: name;
}
Rectangle {}
}
component ThreadSliderComponent inherits HorizontalLayout {
in-out property <float> minimum_number;
in-out property <float> maximum_number;
in-out property <string> name;
spacing: 5px;
callback changed <=> slider.changed;
Text {
text <=> name;
vertical-alignment: TextVerticalAlignment.center;
height: SettingsSize.item_height;
}
slider := Slider {
enabled: true;
height: SettingsSize.item_height;
minimum: minimum_number;
maximum <=> maximum_number;
value <=> Settings.thread_number;
}
Text {
height: SettingsSize.item_height;
vertical-alignment: TextVerticalAlignment.center;
text: round(slider.value) == 0 ? ("All (" + GuiState.maximum_threads + "/" + GuiState.maximum_threads + ")") : (round(slider.value) + "/" + GuiState.maximum_threads);
}
}
component MinMaxSizeComponent inherits HorizontalLayout {
spacing: 20px;
Text {
horizontal-stretch: 0.0;
text:"Items Size(Bytes)";
vertical-alignment: TextVerticalAlignment.center;
}
HorizontalLayout {
spacing: 5px;
horizontal-stretch: 1.0;
Text {
text:"Min:";
vertical-alignment: TextVerticalAlignment.center;
}
LineEdit {
height: SettingsSize.item_height;
text <=> Settings.minimum_file_size;
}
Text {
text:"Max:";
vertical-alignment: TextVerticalAlignment.center;
}
LineEdit {
height: SettingsSize.item_height;
text <=> Settings.maximum_file_size;
}
}
}
component Presets inherits Rectangle {
property <bool> edit_name;
property <string> current_index;
if !edit_name: HorizontalLayout {
spacing: 5px;
Text {
text : "Current Preset:";
vertical-alignment: TextVerticalAlignment.center;
}
combo_box := ComboBox {
current-index <=> Settings.settings_preset_idx;
model: Settings.settings_presets;
selected(item) => {
Settings.settings_preset_idx = self.current_index;
Callabler.changed_settings_preset();
}
}
Button {
text: "Edit name";
clicked => {
root.edit_name = !root.edit_name;
}
}
}
if edit_name : HorizontalLayout{
spacing: 5px;
Text {
text: "Choose name for prefix " + (Settings.settings_preset_idx + 1);
vertical-alignment: TextVerticalAlignment.center;
}
current_name := LineEdit {
text: Settings.settings_presets[Settings.settings_preset_idx];
}
Button {
text: "Save";
clicked => {
Settings.settings_presets[Settings.settings_preset_idx] = current_name.text;
edit_name = false;
}
}
}
}
// component Language inherits HorizontalLayout {
// spacing: 5px;
// Text {
// text: Callabler.translate("settings_language", []);
// vertical-alignment: TextVerticalAlignment.center;
// }
// ComboBox {
// model: ["English"];
// }
// }
component HeaderText inherits Text {
font-size: 15px;
height: SettingsSize.item_height;
horizontal-alignment: TextHorizontalAlignment.center;
vertical-alignment: TextVerticalAlignment.center;
}
component ConfigCacheButtons inherits HorizontalLayout {
spacing: 20px;
Button {
text: "Open config folder";
clicked => {
Callabler.open_config_folder();
}
}
Button {
text: "Open cache folder";
clicked => {
Callabler.open_cache_folder();
}
}
}
export component SettingsList inherits VerticalLayout {
preferred-height: 300px;
preferred-width: 400px;
in-out property <bool> restart_required;
Text {
text: "Settings";
height: SettingsSize.item_height;
horizontal-alignment: TextHorizontalAlignment.center;
font-size: 20px;
}
ScrollView {
VerticalLayout {
padding-right: 15px;
padding-bottom: 10px;
spacing: 5px;
Presets{
height: SettingsSize.item_height;
}
// TODO Maybe someday
// Language {
// height: SettingsSize.item_height;
// }
HeaderText {
text: "General settings";
}
TextComponent {
name: "Excluded item:";
model <=> Settings.excluded_items;
}
TextComponent {
name: "Allowed extensions:";
model <=> Settings.allowed_extensions;
}
MinMaxSizeComponent {
}
CheckBoxComponent {
name: "Recursive";
model <=> Settings.recursive_search;
}
CheckBoxComponent {
name: "Use Cache";
model <=> Settings.use_cache;
}
CheckBoxComponent {
name: "Also save cache as JSON file";
model <=> Settings.save_as_json;
}
CheckBoxComponent {
name: "Move deleted files to trash";
model <=> Settings.move_to_trash;
}
CheckBoxComponent {
name: "Ignore other filesystems (only Linux)";
model <=> Settings.ignore_other_filesystems;
}
ThreadSliderComponent {
name: "Thread number";
maximum_number <=> GuiState.maximum_threads;
changed => {
restart_required = true;
}
}
if restart_required: Text {
text: "---You need to restart app to apply changes in thread number---";
horizontal-alignment: TextHorizontalAlignment.center;
}
HeaderText {
text: "Duplicate tool";
}
CheckBoxComponent {
name: "Image preview";
model <=> Settings.duplicate_image_preview;
}
CheckBoxComponent {
name: "Hide hard links";
model <=> Settings.duplicate_hide_hard_links;
}
TextComponent {
name: "Minimal size of cached files - Hash (KB)";
model <=> Settings.duplicate_minimal_hash_cache_size;
}
CheckBoxComponent {
name: "Use prehash";
model <=> Settings.duplicate_use_prehash;
}
TextComponent {
name: "Minimal size of cached files - Prehash (KB)";
model <=> Settings.duplicate_minimal_prehash_cache_size;
}
CheckBoxComponent {
name: "Delete outdated entries";
model <=> Settings.duplicate_delete_outdated_entries;
}
HeaderText {
text: "Similar Images tool";
}
CheckBoxComponent {
name: "Image preview";
model <=> Settings.similar_images_show_image_preview;
}
CheckBoxComponent {
name: "Delete outdated entries";
model <=> Settings.similar_images_delete_outdated_entries;
}
HeaderText {
text: "Similar Videos tool";
}
CheckBoxComponent {
name: "Delete outdated entries";
model <=> Settings.similar_videos_delete_outdated_entries;
}
HeaderText {
text: "Similar Music tool";
}
CheckBoxComponent {
name: "Delete outdated entries";
model <=> Settings.similar_music_delete_outdated_entries;
}
ConfigCacheButtons {
}
}
}
HorizontalLayout {
spacing: 5px;
Button {
text: "Save";
clicked => {
Callabler.save_current_preset();
}
}
Button {
text: "Load";
clicked => {
Callabler.load_current_preset();
}
}
Button {
text: "Reset";
clicked => {
Callabler.reset_current_preset();
}
}
}
}

View file

@ -0,0 +1,99 @@
import { Button, VerticalBox ,TextEdit, HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, LineEdit, ScrollView, ComboBox, Slider} 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";
import {PopupNewDirectories} from "popup_new_directories.slint";
import { PopupSelect } from "popup_select.slint";
component ComboBoxWrapper inherits HorizontalLayout {
in-out property <string> text;
in-out property <[string]> model;
in-out property <int> current_index;
spacing: 5px;
Text {
text <=> root.text;
vertical_alignment: TextVerticalAlignment.center;
}
ComboBox {
model: root.model;
current_index <=> root.current_index;
}
}
component CheckBoxWrapper inherits CheckBox {
}
component SubsettingsHeader inherits Text {
text: "Subsettings";
font-size: 15px;
}
component SliderWrapper inherits HorizontalLayout {
in-out property <float> maximum;
in-out property <float> value;
in-out property <string> text;
in-out property <string> end_text;
in-out property <length> end_text_size;
spacing: 5px;
Text {
text: root.text;
}
Slider {
min-width: 30px;
minimum: 0;
maximum <=> root.maximum;
value <=> root.value;
}
Text {
text: root.end_text;
width: root.end_text_size;
}
}
export component ToolSettings {
ScrollView {
if GuiState.active_tab == CurrentTab.SimilarImages: VerticalLayout {
spacing: 5px;
padding: 10px;
SubsettingsHeader { }
ComboBoxWrapper {
text: "Hash size";
model: Settings.similar_images_sub_available_hash_size;
current_index: Settings.similar_images_sub_hash_size_index;
}
ComboBoxWrapper {
text: "Resize Algorithm";
model: Settings.similar_images_sub_available_resize_algorithm;
current_index: Settings.similar_images_sub_resize_algorithm_index;
}
ComboBoxWrapper {
text: "Hash type";
model: Settings.similar_images_sub_available_hash_type;
current_index: Settings.similar_images_sub_hash_type_index;
}
CheckBoxWrapper {
text: "Ignore same size";
checked: Settings.similar_images_sub_ignore_same_size;
}
SliderWrapper {
text: "Max difference";
end_text: "(" + round(Settings.similar_images_sub_current_similarity) + "/" + round(Settings.similar_images_sub_max_similarity) + ")";
end_text_size: 40px;
maximum <=> Settings.similar_images_sub_max_similarity;
value <=> Settings.similar_images_sub_current_similarity;
}
Rectangle {}
}
}
}