mirror of https://github.com/qarmin/czkawka
Alternative frontend with Slint (#1102)
parent
8df5e991a6
commit
c6b1eaeeb4
@ -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' }}
|
File diff suppressed because it is too large
Load Diff
@ -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.
|
@ -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
|
@ -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.
|
@ -0,0 +1,3 @@
|
||||
# Czkawka Core
|
||||
|
||||
Core of Czkawka GUI/CLI and Krokiet projects.
|
@ -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.
|
@ -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"
|
@ -1,3 +0,0 @@
|
||||
fn main() {}
|
||||
|
||||
slint::slint! {}
|
@ -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"]
|
@ -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.
|
@ -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.
|
@ -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();
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -0,0 +1 @@
|
||||
settings_language = Languages
|
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -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 |
@ -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<_>>())
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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))
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
@ -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()
|
||||
});
|
||||
}
|
@ -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))
|
||||
// }
|
@ -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
|
||||
// }
|
@ -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));
|
||||
}
|
@ -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
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export component Preview inherits Image {
|
||||
|
||||
}
|
@ -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 + "%";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue